Conversation
Embellishments were driven by three hardcoded lists that had drifted apart from each other and from EmbellishmentDB, so items were silently dropped rather than scored: - getItemEffectOptions offered seven embellishments and excluded Finger/Neck outright, so jewellery embellishments could never be attached. Now derived from EmbellishmentDB via new applicable / slots metadata. - The SimC importer matched incoming spell names against an allowlist of eight names and discarded everything else, so an imported item lost its embellishment and scored as a plain stat stick with no error. Now resolves against the same DB. The two entry points also disagreed with each other: Sunfire Silk Lining imported but could not be added manually, and Adorned Fang was the reverse. - Crafted items that carry an embellishment inherently (Axe-Flingin' Bands, the World Tender's set) have no effect block in ItemDB and simmed as plain gear. Item now attaches those from EmbellishmentDB.setItems so a gap in ItemDB can't silently drop them again. - Multi-piece embellishment sets are granted once, only when enough pieces are worn, and consume embellishment slots correctly. - Embellishments with no formula are flagged unmodelled and withheld from the dropdown rather than silently scoring zero. A test fails if a formula lands and the flag isn't removed. - The crafted stat picker is hidden on items with no assignable budget, where choosing stats did nothing. Weapons: buildNewWepCombos only emitted a one hander when it could pair it with an offhand. With no offhand the weapon was invisible to Top Gear, and if it was the player's only weapon there were zero combos, zero sets and an empty report. Unpaired one handers are now offered alone, with report advice noting the offhand is scored as empty. Embellishment cap: exceeding two embellishments makes every set fail verifySet. That's correct but was silent, so the item was dropped from all results or the whole report came back empty. The selection screen now warns when the cap makes a set impossible, the report explains when a selected embellished item couldn't be used, and the engine reports the error rather than returning a bare null. Also fixes a latent crash where getSetting returns 0 for a missing setting and the result was used as a string (darkmoonHuntStat, and flaskChoice which would take out an entire Top Gear run on stale settings), and moves createTopGearWorker into its own module so TopGearEngineShared can be imported from tests without tripping over import.meta. 44 suites / 233 tests passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
…er mastery effectiveness Settings are grouped by a category field, but the panel rendered a hardcoded list of categories. A setting whose category was missing from that list was stored in Redux, fully wired to the engine, and never drawn, with no error anywhere. The list now lives in one exported place and SettingsCategories.test.js fails if any category with settings is missing from it, if a listed category is empty, or if a Retail setting has no title and tooltip. That last check caught a pre-existing empty tooltip on enchantItems, which is now filled. Categories with nothing in them for the current game type no longer draw a bare heading, and one with no settings at all can't crash the panel. Food, weapon oil and Vantus Rune were applied unconditionally with no way to turn them off. They're toggles now. Only options with real modelled values are offered - there is no food data beyond a single hardcoded entry, so that dropdown is deliberately limited rather than invented. Mastery effectiveness for Preservation Evoker was hardcoded at 0.9 inside scoreEvokerSet, overwriting whatever was passed in. Preservation mastery scales with how injured the target is so its real effectiveness varies a lot by content, and Resto Shaman already exposed this as a setting. Absent settings keep 0.9 so existing results are unchanged. The value is read with Number() rather than a typeof check because the settings panel writes number inputs back as strings - a strict typeof === "number" check worked on the reducer's initial value and then silently fell back to the default the moment the player edited the box, which looked exactly like the setting doing nothing. 42 suites / 200 tests passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
hardScore is an intellect-equivalent ranking number, so the report could only ever show relative differences - and the competitive alternatives list was printing a hardScore delta labelled "HPS", which it isn't. Sets evaluated through a cast model or a ramp sim now carry setHPS, a genuine healing figure. It's shown on the stat panel with an upgrade percentage over currently equipped gear, and per alternative alongside the healing and percentage given up. The percentage is derived from the HPS figures themselves so the two can never disagree. The stat weight path deliberately reports nothing rather than a fabricated number. baseHPS there is a hardcoded placeholder, and that path pre-applies raid buffs which the cast models also apply internally, so anything derived from it would be wrong. Those specs keep the existing relative display. The equipped set is evaluated through the same evalSet as the candidates so the two figures are comparable, and deliberately outside the ranking loop so it can never influence which set wins. It's wrapped so that a malformed equipped set costs the upgrade percentage rather than the whole run. A dead-on 0% - which happens when the player runs Top Gear without adding any candidate items, so the best set is simply their current gear - now says so instead of reading like the comparison failed. 41 suites / 204 tests passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Combines the three PRs (Voulk#1788, Voulk#1789, Voulk#1790) for anyone who wants to run or review all of it together. Each remains independently mergeable on its own branch; this is the union, not a replacement. Conflicts were all adjacent additions to the same structures - embellishedSelected next to equippedHPS on the result object and in the saved report - so both sides were kept. Merging also removed two things that only existed to keep the HPS work standalone: the stub for TopGearEngineShared is unnecessary once the worker factory split is present, so the real module is imported again and the shared buildDifferential is unit tested directly, and the embellishedSelected assertion is restored now that both halves are in the same tree. 47 suites / 261 tests passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Puts these choices where item selection already lives rather than in the global settings panel, since they're choices about the set being built. Each Top Gear run uses exactly one configuration. Enchants were previously inlined in TopGearEngine.enchantItems, one hardcoded per slot with its stat values written into the branch. They're extracted into EnchantDB, which is now the single source of truth: the engine applies from it and the selection UI offers from it, so the two can't drift apart. Nothing was invented - the table holds exactly the enchants and values the engine already applied, which is why Eyes of the Eagle appears (it was already modelled for Holy Priest and Resto Shaman, just not selectable). EnchantDB.test.js pins every value against what the engine applied before extraction. Gems can be picked for the meta socket and the stat sockets independently, from the current expansion's gems. The replace / fill-empty switch is genuinely new behaviour rather than a toggle over the old one. Top Gear has always ignored the player's socketed gems and assigned its own to every socket, which quietly assumes they will re-gem the whole set. Unticked, gems already socketed are kept and only empty sockets are filled, so the result is what the player would get without spending anything. Equipped gems come from the SimC import, which stores them on the item as a colon separated gemString; an unrecognised gem is treated as an empty socket. Folio slots 1, 4 and 5 are selectable. Slots 2 and 3 have a single rune each so they aren't offered. Everything defaults to Automatic, which resolves to the pick the engine made before any of this existed, so an untouched profile produces identical results. These settings are marked type "hidden" because they're owned by this section rather than the auto-rendered settings panel. SettingsCategories.test.js was scoped to panel-rendered settings so it still guards the panel without forcing these into it. 49 suites / 288 tests passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
…l gems The current tier's stat gems were recorded as 12 major / 5 minor. They're 16/9, so every gem in the app was undervalued and the split between major and minor was wrong. The ratio is now published as GEM_MAJOR_STAT and GEM_MINOR_STAT constants so the engine's gem lookup reads from one place and a future tier is a single change. Two duplicated rows (Flawless Versatile Amethyst and Flawless Masterful Peridot) are removed while in there. This changes results for everyone, since gems are worth more than the app previously thought. Gem selection is now multi-select. Picking several expands each gear set into one candidate per gem loadout, which are then ranked together - the same way selecting two rings gives you two candidate sets to compare, rather than the engine searching for a winner behind the player's back. Picking one leaves it at a single evaluation per set, exactly as before. Sockets are interchangeable for stat purposes, so only the multiset matters: two of gem A and one of gem B is the same stat line whichever socket holds which. Loadouts are therefore combinations rather than permutations, which keeps the expansion small, and it's capped at 12 regardless - every loadout multiplies the sets evaluated, and the whole gem spread is worth a fraction of a percent. 49 suites / 300 tests passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
…together Enchants can now be picked several at a time per slot, the same as gems. Selecting more than one expands each gear set into a candidate per combination, and those are ranked together - so choosing two weapon enchants gives you two sets to compare rather than the engine deciding for you. Gems and enchants expand into one combined list of variants, each a complete wearable configuration. The expansion is capped at 24 variants: every variant re-evaluates every gear set, so an uncapped cross product of a few multi-selects would make a run take minutes. The UI says so where the choices are made. Slot cards get DEFAULT (clear the selection and let the engine pick) and NONE, matching how these are usually presented. 49 suites / 309 tests passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Top Gear now shows only a Detailed switch by default, and the extra selection panels appear when it's turned on. The toggle also gates the engine: while it's off every one of these settings reads as its Automatic default, so a profile that was configured once and then switched back to the simple view can't keep steering later runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
The expansion was capped at 12 gem loadouts inside 24 total variants. That keeps a run fast but truncates the search, so the best combination can be one the run never evaluates. gearVariantLimit turns the cap into a setting, and a limit of 0 removes it entirely for full combinatorics. The default is unchanged: leaving the setting alone reproduces the old 12/24 behaviour exactly. Raising it past the default raises the gem cap too. Exhaustive searches are easy to make unrunnable by accident, so the panel projects the real variant count from the current selection - the same arithmetic the engine does, via countGemLoadouts/countEnchantCombinations - and says how many will actually run before the player commits to one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Folio slots took a single rune, so finding the best combination meant running Top Gear once per rune and comparing by hand. They now hold a list, and selecting several expands the run into one variant per combination, ranked alongside every other set - the same treatment gems and enchants already got. buildSetVariants gains a third axis, so gems, enchants and runes all multiply together under one search-depth limit, and the panel's projection counts all three. The settings held a bare shortName before this and saved profiles still do, so getFolioChoices accepts either form. A slot with several runes pinned and no variant driving it resolves to Automatic rather than quietly taking the first pick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Gems, enchants and Folio runes each had their own copy of the same cartesian product over per-slot choices, plus its own counting function. They now share buildChoiceCombinations / countChoiceCombinations in ItemUtilities, which both the engine and the Folio data already import from. The two enchant resolvers duplicated the same lookup block; getSlotEnchant now calls getPinnedEnchant. Renamed from getChosenEnchantId, which returned an enchant rather than an id. The selector had three near-identical dropdowns - two helpers plus an inlined one for gems - collapsed into a single select() with a `multiple` flag. Gem lists are built once at module scope instead of filtered and de-duped on every render. Trimmed comments that restated the code and dropped the unused DETAILED_GEAR_OPTIONS export. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
…unes A true/false switch in the Top Gear settings. On, the engine searches every current stat gem, every enchant in every slot and every Folio rune alongside its normal gear search, instead of the player pinning each one by hand. It supersedes the pins rather than writing over them, so switching it back off returns whatever was chosen before, untouched. The gear panel opens for it even with the Detailed toggle off - the search depth it needs would otherwise be unreachable - and says the pins are being ignored while it runs. The engine's own automatic pick is entered as a candidate whenever this is on. A capped search keeps the first N combinations in build order rather than the best N, so without that a run could rank below the plain run it was meant to improve on; a test pins that at several limits. Gem list filtering moved to GemDB as getCurrentStatGems/getCurrentMetaGems, which the panel and the search space now share. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
A run could take anywhere from a second to several minutes depending on how many items are selected and how wide the gem, enchant and rune search is, and the only feedback was the button going grey. runTopGear now takes an optional progress callback and reports about a hundred times over a run - enough to fill a bar smoothly without the postMessage traffic becoming the cost itself. The worker forwards those to the page, which shows the stage, the evaluation count and an estimate of the time left. The count is evaluations rather than sets, since variants score each set once per configuration and that multiplier is what makes a run long in the first place. The estimate waits for a second of real work so the rate isn't measuring startup. Runs without a callback are unchanged, which is every caller except the worker. Classic still has no progress; it splits across several workers and would need its own aggregation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Slots 1 and 5 were selectable but not worth choosing between: slot 5's runes are all unmodelled (their runFunc returns no stats, so every pick scored the same) and slot 1's are procs the engine already picks well. Offering them multiplied the search by six for nothing. Only slot 4, the secondary stat slot, is selectable now; the rest resolve to the engine's automatic pick as they always did. The dropdown is labelled by what it is rather than by slot number. FOLIO_SLOT_SETTINGS is the single place that decides this, so the search space, the combination expansion and the panel all followed from the one change. The folioSlot1 and folioSlot5 settings are gone rather than left dead in the profile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
It was a dropdown inside the collapsed settings accordion, which is a poor home for something that changes what an entire run does - easy to miss, and easier still to leave on without noticing. It's now a switch on the settings header itself, visible without expanding anything. The setting is marked hidden so the panel no longer draws it too. Gated behind an optimizeToggle prop threaded from Top Gear, since the character panel that hosts these settings is shared with Quick Compare, Trinket Analysis and several other pages where the switch would do nothing. Its clicks stop propagating so hitting the switch doesn't also expand the panel underneath it. Hidden settings are exempt from the labelling test, so there's a new check that this one keeps its title and tooltip strings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
The minor stat was set to 9. It's 7. The twelve gem rows repeated the numbers as literals while GEM_MAJOR_STAT and GEM_MINOR_STAT sat above them unused by the data, which is how the two came to disagree. The rows now reference the constants, so a tier update is one edit. That drift is worth guarding: the engine's gem lookup searches by those exact values, so a mismatch finds nothing and silently sockets a placeholder gem rather than failing. Three tests now pin the split, check every current gem grants it, and check a run never sockets the fallback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
canBeCatalyzed also required the SimC import to have tagged the item with the current season's item_conversion field. Anything added by hand, imported without that field, or carried over from an earlier season had no catalyst option at all - the menu entry simply never appeared. Slot is the only thing that decides eligibility now. Still excluded: pieces that are already tier, and pieces already through the catalyst. Those aren't about origin. The conversion keeps the piece you had - stats, item level, socket, tertiary and embellishment all carry over, and only the item's identity changes. The copy Player.catalyzeItem adds now takes a copy of the stats rather than sharing the object, since addStats mutates in place and reached back into the original. convertToTier returns false instead of half-converting when a spec has no tier piece for the slot, and catalyzeItem reports rather than silently doing nothing. A test checks every spec has a piece in all five slots, since a gap there means the player ticks catalyze and sees no change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Two problems, both from the variant work. Progress updates were throttled by count - about a hundred per run - which is right over a minute and wrong over a second. Each update re-renders the Top Gear page and every item card on it, so a run with few sets paid a hundred full re-renders in the time it took to do a few dozen evaluations, and the fewer sets there were the worse that ratio got. Updates are now limited to ten a second, with the last always taken so the bar can't freeze short of the end. Close Alternatives rendered as a bare score with nothing beside it. Three causes: - The differential only compared items and gems. Sets expand into one candidate per gem, enchant and rune combination, so most alternatives now differ by an enchant or a Folio rune, which nothing looked at. Both are reported and drawn now, named rather than iconned. - The gem comparison tested membership, so "three A one B" against "two A two B" read as no difference. It counts occurrences now. - Gem loadouts were built one gem too long. Socket 0 is the meta and is chosen separately, so a loadout only ever fills sockets - 1; the extra entry was never read and every loadout had siblings differing only in it. Those duplicates scored identically, wasted evaluations, and surfaced as alternative rows with nothing to show. On the test gear this halved the real search space from 288 variants to 144. A set that still comes out wearing exactly what the best set wears is skipped rather than shown, and the loop keeps scanning for a real alternative instead of spending one of the twelve slots on it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Rings were one choice applied to the pair, and applied once. Two things follow from that being wrong. The 29 in the ring enchant rows is one ring's worth - the stale "64 x 2" comment this carried over from said so, just with a figure from an older tier. A set wears two rings, so a set running the same enchant on both now gets 58 rather than 29. That lifts every set by the same amount, so rankings barely move, but the absolute HPS figures were under-counting. Rings are now two independent slots, Finger1 and Finger2, both drawing from the one Finger enchant list since the game has no left-ring enchant. They expand as two axes, so offering four enchants on both is sixteen combinations rather than four - which is the point, because secondaries diminish and 29 crit plus 29 haste can beat 58 of either. Under Optimize Everything this multiplies the search accordingly. Profiles saved before the split hold a single Finger key; normaliseEnchantChoices spreads it across both rings, and an explicit per-ring choice wins over it. The report cards share a slot name between the two rings, so each is told which entry is its own, falling back to the shared key for reports saved earlier. Close Alternatives names the ring that changed rather than "Rings". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Both become search axes on the same terms as gems, enchants and Folio runes: pin several in the gear panel and each combination is ranked as its own set. They differ from the others in what an empty selection falls back to. Gems and runes fall back to the engine's own pick; these fall back to the single dropdown in the settings panel, which is what an untouched profile uses and how the simple path has always worked. Nothing changes until something is pinned. buildSetVariants now takes its axes by name rather than by position. This is the fourth one, and adding it positionally would have meant every caller passing empty arrays through for the axes it doesn't care about. Only Intellect Food is modelled, so food is a yes/no choice for now - the panel says as much. The flask list is the four stat flasks; they all grant the same amount so only the stat differs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
71.5 of the player's highest weighted secondary, against the plain intellect food's 50 intellect. That makes food a real choice for the first time, so it's worth searching rather than a yes/no. Food is now a table rather than an if: a row in FOOD_BUFFS plus a name in CONSUMABLE_OPTIONS is all another one needs. A stat of "bestSecondary" is resolved per spec at evaluation time, since which secondary that is depends on the player's weights - the same figure the automatic flask already followed, now hoisted out of that branch so both use it. An unrecognised food falls back to the plain intellect one rather than dropping the buff, which is what a profile holding a stale name would otherwise get. The report now names the food it picked, as it already did for the flask - a set winning on the strength of its food would otherwise never say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Top Gear pushed every scored set into an array and only sorted and pruned it once the whole run had finished. Since only the best 3000 are ever read, a large search spent gigabytes holding sets it was about to discard: each scored set retains ~2.5kb, so a 7.8m evaluation run needs ~20GB and the worker died around a quarter of the way in, leaving the progress bar frozen on its last report. TopSets keeps a running top slice instead, admitting a set only once it beats the current cut-off and trimming in batches. This picks the same sets the old sort-then-slice did: hardScore is final the moment evalSet returns and is never touched again, and the top N of a stream by a fixed key is the top N of the fully sorted list. Sets tied on score can swap places, since the comparator never returns 0 and leaves ties unordered either way. Measured over 355,806 evaluations with GC forced at each sample: live heap growth across the run drops from 876MB to 1MB, peak from 1176MB to 318MB, and the winning set, its gems, enchants, score and all twelve differentials are unchanged. Also stops evaluating the remaining variants of an unwearable set. Validity depends on the items worn, not the gems, enchants or consumables layered over them, so the first variant settles it for the rest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Optimize Everything is a "search all of it" switch, so having it also depend on a separate depth dropdown meant it quietly delivered a truncated search unless you knew to go and raise that too. It now sets its own depth, and the dropdown is disabled while it's on so a value left in an older profile can't truncate the run either. No limit is also the new default for the gear panel. An untouched profile expands nothing - with no gems, enchants or runes multi-selected there is exactly one variant - so this only bites once the player has asked for combinations, and at that point they should get all of them rather than the first 24. The toggle-off path still resolves to 24, so a profile that never opened the panel runs exactly as it did before any of this existed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Single-stat gems: each element family had three hybrids and was missing its own self-matching adjective, so the four solo gems slot straight in at 17 in one stat. Worth more than a hybrid's major stat, so they win outright when one stat is far enough ahead to be worth giving up the split - which the optimiser could never find while they weren't in the database. Sharding: runTopGear splits into runTopGearShard, which evaluates a slice of the gear sets, and finishTopGear, which merges the shards into the report. Retail now runs four workers instead of one. Sets are dealt out round robin rather than in blocks, since neighbouring sets are near-identical in build order and a block split would hand one worker all the good sets. The merge is exact: each shard keeps its own best 3000, and re-ranking their union keeps the same sets one thread would have, because a set only misses the cut if 3000 better ones exist and every one of those is in some shard's slice. Tests pin the winning set, the differentials and the compared count against an unsharded run at two, three and four ways. Report highlighting: items you aren't wearing and gems you don't have socketed get a gold outline. It's an outline rather than a border so it composes with the vault, exclusive and catalyst styles that already own the border, and gem comparison is skipped on an item that's new anyway. Needed carrying gemString through shortenReport, which whitelists fields and was dropping it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
A large selection spends real time building sets before a single one is evaluated, and the bar sat indeterminate through all of it with no way to tell a slow start from a hang. The total is exact rather than an estimate. Nothing but the ring and trinket pair rules rejects a combination, so the set count is the product of the other slots times the valid pairs in those two. Both rules moved into shared helpers that the count and the building loops now share, so the bar can't promise a total the loops never reach - which would leave it stuck short forever. A test pins the promised total against the number of sets the evaluation stage then reports. Two fixes the new stage exposed. The estimate now measures the current stage rather than the whole run: carrying the run's elapsed time into a stage that has just started read as hours remaining for its first few updates. And shards are only summed with others in the same stage, since a shard's set count and another's evaluation count don't add up to anything - the run is as far along as its slowest worker. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Progress reporting isn't what costs time: measured over a 67 second run, a run with a progress callback and one without differ by 0.5%, inside the noise, because the engine reports about a hundred times regardless of how long it takes. There is nothing to gain by reporting less often. Measuring that did turn up real waste. Sharding split the evaluation but not the build: every worker built the whole set list and then filtered away the three quarters it didn't own. createSets now takes the shard and skips assembling the item list, scoring it and allocating an ItemSet for sets that belong to another worker. It still walks the whole combination space, which is only index arithmetic, so the sets keep the same ids and the shards divide them exactly as before. Measured over 589,824 sets: 1328ms to build them on one shard, 366ms for one of four - a 3.63x speedup on a stage that previously ran at full cost on every worker simultaneously. Worker count now scales with the machine, up to eight, leaving one core for the thread drawing the progress bar. Capping at four made sense when extra workers each repeated the whole build; now they don't. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
A worker is not free. Each one parses the engine bundle and builds every database before it evaluates a single set, measured at ~618ms of pure startup, and the chunk it loads is 10MB. Going from one worker to eight therefore added that cost eight times over to every run, however small - which is exactly the fixed floor that made short runs feel slower than they used to be no matter the set size. Runs are now sized before any worker is spawned, and workers are added only as fast as there is work to pay for each one's startup: one worker per 50k estimated evaluations, up to the core count less one. A small run gets a single worker again, and a run big enough to need eight still gets eight. countGearSets counts the sets a selection produces without building them - exact, since only the ring and trinket pair rules reject a combination, and createSets now uses it for its own total so the two can't drift. estimateEvaluations multiplies that by the variant count. Sockets are taken per slot rather than per set, so it's an estimate, but it only picks a worker count: being out by a factor of two changes how fast a run is, never what it returns. For the record, an A/B on identical work - same gems, enchants and depth - puts this session's engine at 285ms against 387ms before it, so the engine itself got 26% faster. The slowdown was startup cost and search space, not evaluation speed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Progress was held in TopGear's own state, so every report re-rendered the whole page: the item bar, the character panel, and the gear options panel - which recomputes the entire enchant and gem search space on every render to project its combination count. The selected item list is built inline in the JSX too, so that panel got a fresh array identity each time and could never skip the work. Ten of those a second, on the one thread the workers are already competing with for cores, is the run paying for its own progress bar. My earlier measurement putting reporting at 0.5% was taken in node with a plain callback - no postMessage, no React, no contention - so it could not have seen this. The bar is now its own component holding its own state, updated through a ref, so a report repaints the bar and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
createSets built every set into an array before a single one was scored. Measured at ~790 bytes per set, that is 2.6GB for 3.5 million sets and over 7GB for ten million - past what a browser worker's heap allows, so a large selection killed the worker while it was still building. Same shape as the bug that used to freeze runs, one layer up: the results were bounded but the set list was not. forEachGearSet hands each set to the evaluator as it is made and keeps nothing, so a set is scored and discarded before the next is built and memory no longer depends on the size of the search. Over 3,515,625 sets, live heap growth drops from 2633MB to 18MB. Two things this needed, both exact rather than estimated. The socket ceiling now comes from the best-socketed item in each slot instead of from the built sets - slots are chosen independently, so that is the same number. The run's total comes from countGearSets, so the progress bar knows the real total from its first report rather than after the build. Also drops the sort by soft score. pruneItems has been commented out for a long time and TopSets is order independent, so it had no consumer left and was O(n log n) over millions of sets for nothing. Building and evaluating are now interleaved, so there is no separate building stage to report - the setup before it starts is now "Preparing", and evaluation reports the full total from the outset. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
SimC reports the Folio as omnium_talents and the importer ignored the line entirely, so the app assumed the automatic runes: the report diffed the result against that assumption rather than against what the character has, and "keep what I have" had nothing to keep. The entry ids are a different numbering to the rune spell ids the rest of the app uses, so they need an explicit mapping. Slot 4 is the only slot whose rune varies - the others are fixed - so it is the only one worth mapping, and 136819 is confirmed as Rune of Critical Power from an export whose owner confirmed crit was selected. Entries not in that table are ignored and the slot falls back to the automatic pick, which is exactly what happened before. Confirming or extending the table is one export: change the rune in game, re-export, and see which number moved. keepsExistingGear moved to ItemUtilities so the Folio can read it. Living in TopGearEngine would have made the import a cycle, since the engine already imports the Folio data. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
The previous commit mapped 136819 to the crit rune, inferred from its position in a single export. That was wrong. Exporting the same character once per rune shows only the second entry ever moves - 136815 for crit, 136821 haste, 136818 mastery, 136820 versatility - while 136814, 136817, 136819 and 136822 stayed identical across all four. 136819 is one of the fixed entries and never encoded a choice at all. All four are now mapped from those exports, so any character's Folio can be read rather than only one stat. The tests carry the four export lines themselves, and assert that none of the entries that never moved is mistaken for the stat rune - which is exactly the mistake the guessed mapping made. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Both answer the same kind of question - what a whole run is allowed to do - so they belong together and in sight, rather than one on the header and the other down inside the panel it opens. The two switches now come from one helper instead of a hand written block each, since they differ only in which setting they read. They right align as a pair, and both still stop their clicks from expanding the accordion under them. The setting is hidden again, meaning owned by a dedicated section rather than the auto-rendered panel, and added to the list of hidden settings the labelling test checks - it's still drawn with a translated title and tooltip, so losing either would show the raw key. Its label and tooltip now mention runes, since it has covered the Omnium Folio since the import landed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
SimC reports them on a commented line, so nothing looked at it. They're the budget any "what should I upgrade" question has to be answered within, so reading them is the first thing that has to work, and it's the same whatever that question turns into. The ids are kept as they appear rather than named. Which id is which crest tier is game data the app doesn't carry, and attributing someone's crests to the wrong tier is worse than showing a number with no name yet. Items on that line are kept apart from currencies, since they aren't spent the same way. Nothing consumes this yet - the cost of an upgrade, in crests, is a second table the app doesn't have either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Its own option rather than part of the replace toggle: that one is about what Top Gear may change for free, this is about what the character can afford to buy. Off by default. The planner takes the affordable upgrade that gains the most healing per crest, then looks again, so the answer is an ordered list of what to buy next rather than one target set that may be several weeks away. Ranks are sequential, so only an item's next unbought rank is ever a candidate, and Valorstones cap the plan alongside crests. Greedy isn't provably optimal - a cheap rank can unlock a better expensive one - so it's a spending order, not a claim of the best reachable outcome. Healing is supplied by a callback, so the planner never scores a set itself and is tested against a stand-in cost table rather than the real one. The crest tiers are now named: 3442 Adventurer, 3443 Veteran, 3444 Champion, 3445 Hero, 3446 Myth, confirmed by matching a character's exact in-game totals against their export. All five amounts were distinct so each id had one tier it could be, and two exports agree - the Adventurer and Myth counts moved between them while the rest held. Worth noting the guess I would have made from the earlier export was wrong: Myth is 3446, not the 3444 that was nearest to a remembered "around 80". The cost of each rank is still missing, so hasCrestData is false and the planner returns no plan rather than a wrong one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Valorstones are gone from the game, so they are gone from the model: the planner tracks crests only, and one budget rather than two. Every rank costing the same means the cost table can be derived rather than typed out. A rank is one step up the item level ladder, stopping at the track's cap, paid for with the crest named after the track - so the only things to keep in step are the ladder and the caps, which the app already had, and a season that changes either needs no edit here. The planner still divides gain by cost rather than ranking on gain alone. With a flat price those come out identical, but the budget is per tier, and the day a rank costs something different this keeps ordering correctly rather than silently going wrong. The crafted tracks stay unpriced. They cap out like any other but no crest is named after them, so crafted items are left out of a plan rather than being charged a guessed currency. Verified against a real export end to end: the budget reads as the character reported it, and the ranks left match their gear - nothing above a track cap, five ranks on a Myth waist at 318, none on a Hero head at 321. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Crafted gear is made at 305 for no crests, and a single payment of 80 lifts it to its track's ceiling - Hero crests for a Runed piece at 318, Myth for a Gilded one at 331. That's a different shape to every other track, which climbs the item level ladder a rank at a time at 20 crests each, so the two crafted tracks are written out while the rest stay derived. They're also the only tracks that spend a crest not named after them, which is why they can't be folded into the same derivation. Nothing plans a crafted upgrade yet, for a reason that sits upstream: the importer tags these tracks from bonus ids 12052 and 12053, and neither appears on this season's crafted items, so they arrive with no track at all. Both crafted pieces on the character this was checked against are already at 331 so it changes nothing for them, but a crafted piece below its ceiling would be missed. Fixing that needs the bonus id that marks each crafted tier now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
planUpgrades scores each candidate rank through the same evalSet as everything else, so a rank is worth what it gives this character rather than its stat gain on paper. Items are copied before being raised, so working out what an upgrade would be worth never touches the player's own gear. Purchases already planned are applied before the next is valued. Gear diminishes, so measuring every step against the original set would keep overvaluing later ones - and it isn't only about that item, since buying a crit piece raises what a haste piece is worth. That interaction is why the finished list isn't in descending order of efficiency, which cost a test assumption before it cost anything else. Scores are cached by the levels the set is wearing, since the search asks about the same combination repeatedly. Crafted tracks renamed to Hero Crafted and Myth Crafted, after the crest that pays for them. The Runed and Gilded names are the previous expansion's and are kept in the cap table and the cost table so gear saved under them still resolves and is still priced. The set scoring tests live in their own file: the mocks in CrestSpending's replace the cost table for that whole file, and these need the real one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
A run with the option on now plans what to buy and the report draws it: each purchase in the order to make it, what it lifts the piece from and to, its cost, and the healing it gains, with a running total underneath. Planned against the player's own Item objects rather than the report's copies. Those have been through postMessage and lost the methods needed to work out what raising them would be worth, so the winning set is matched back by uniqueHash, unfolding weapons the same way shortenReport does. Only the fields the panel draws are kept, so a saved report doesn't carry another copy of every item. The panel is its own component. Partly because the report file is already long, but mainly so it can be rendered in a test: react-scripts build doesn't type check, so an undefined name in JSX compiles cleanly and only fails in the browser, which is how a deleted component shipped earlier in this work. The report itself can no longer be rendered from the bundled sample - that had already drifted past several fields the report reads - which is its own reason for the panel to stand alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
It scored every candidate against a re-gemmed ideal, because it passed Top Gear's settings straight through. So the percentage answered "how much better would this item and a full re-gem make me" rather than the question being asked. It now keeps the character's own gems, enchants and Folio runes, and only the candidate item - which has none of its own - is gemmed and enchanted automatically, like any new drop would be. The gem and enchant expansion is pinned off at the same time. Upgrade Finder runs a full evaluation per candidate, hundreds of times, on the thread drawing the page, and Optimize Everything now resolves to no limit - so leaving it on meant millions of evaluations per candidate for an estimate meant to be rough. That was a regression this branch introduced when Optimize Everything stopped capping at 24 variants, and it reached here through a caller I hadn't checked. Pinning it also keeps the comparison honest: item A only says something about item B if both were gemmed the same way, and a per candidate search can hand one of them a better loadout for reasons that have nothing to do with the item. The settings builder is named upgradeFinderGearSettings rather than the obvious upgradeFinderSettings, which is already a parameter name in buildItem and would have shadowed it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Keeping the character's own gems means a variant's loadout never reaches a socket: every loadout resolves to the same gems, so the run evaluates one identical candidate per loadout. With sixteen gems over four sockets that is several hundred copies of the same answer. That is the shape of slowdown that hides well - the run is busy, the progress bar moves, and none of the work can change the result. Enchants were already gated this way when they're being kept; gems were not. Only when every socket is actually holding a gem the database recognises. An empty socket, or one holding something unrecognised, still falls back to a pick the loadout decides, so the search there is worth paying for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
This reverts commit 2d7f021.
This reverts commit 0cb3ca8.
Workers were spent one per 50,000 estimated evaluations, so a 60k run got a single worker and a 200k run got four. That put the run time back to being linear in the size of the search - the exact thing sharding was for - and it was a mid sized run, not a huge one, that felt it. The flat slice was the wrong shape. Adding the Nth worker takes the run from W/(N-1) to W/N, so it saves W/(N(N-1)): the saving shrinks as workers are added, while each one clears the same fixed startup. Worth having while that saving beats startup, which settles N around sqrt(W / startup). A 60k run now gets three workers instead of one, 100k gets four instead of two, and a run too small to pay for a second still gets one - which is what the flat rule was introduced to fix. Both constants are measured rather than guessed: ~10,000 evaluations a second and ~0.6s of worker startup, both taken from real runs earlier in this branch. The rule moved to ShardProgress so it could be tested. TopGear.tsx can't be imported under jest - the worker factory it pulls in uses import.meta.url - which is the same reason the progress aggregation lives there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
saveAllChar has always written the whole character out, item list included, but init rebuilt each one from a handful of fields and never read the items back. So the gear list emptied on every load, and going back to fine tune a run meant adding and re-ticking everything first. Items are restored by laying the saved fields over a bare instance rather than going through the constructor, which derives stats, sockets and quality from the item id and would undo anything the player had changed - an upgraded level, a chosen embellishment, a catalysed piece. Saving also had to start covering more than the import. Every handler that changes a character's items now goes through one place that refreshes the list and writes them out, so none can quietly skip it - measured at 3.4ms for seven characters carrying two hundred items each, which a click won't notice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
A piece taking two ranks appears twice in the ordered list and reads as two separate decisions, when the question once you've decided to follow the plan is what each piece costs in total. The panel now groups the same plan by piece underneath, adding up the ranks, the crests and the gain. Only when some piece takes more than one rank - otherwise it would just repeat the list above it. Two more fields were being saved and never read back, the same gap the item list had. Without the Folio runes the Folio quietly falls back to the automatic pick even with "keep what I have" on; without the crests there is no budget, so a spending plan comes back empty. Both survive a reload now. upgradeCurrency also had no default on Player at all - an earlier edit that silently didn't apply, which went unnoticed because the import assigns the field anyway. It works either way, but the shape is declared now and a test covers a character that never imported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
A plan could only ever be made against the crests the import happened to read, so there was no way to ask what to save for - what next week's crests would be worth spending on, or whether a tier is worth farming at all. The five amounts are now editable settings. The SimC import seeds them, so they show what the character actually has without anything being configured, and they're the budget from then on - what the import read is only a fallback for a character imported before the boxes existed. Zero is a real answer, so a tier can be excluded rather than only reduced. They sit in a Crests section with the plan toggle rather than under Top Gear, which was already the longest section in the panel and is about how a run is searched rather than what it can afford. The report says what it planned with, since an edited budget is otherwise invisible - a plan that spends 500 Hero crests looks the same whether the character has them or not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
An item added by hand had no upgrade track, because the SimC import reads that from bonus ids and there is nothing to read from a manual entry. Without one the piece offers no upgrades in its cogwheel and can never appear in a crest plan, so there was no way to try "what would a Myth piece here be worth spending on". The track is now a dropdown on the add form and can be changed later from any item's cogwheel. Changing a track alters the item in place rather than adding a copy, unlike an embellishment or a stat combination: the track doesn't change what the piece gives you now, only what it can become, so there are no two versions to compare. The plan now considers pieces that were in the running but lost their slot. A piece has to clear the one it would replace before it gains anything, so planning only the winning set could never buy into one that a couple of upgrades would make the better choice. Each candidate is pinned up front to the weakest piece of its slot - which piece it would displace doesn't change as crests are spent, and deciding it per evaluation would multiply the search by the number of candidates. That also meant costing a whole climb rather than a rank at a time. A piece that needs three ranks to beat the incumbent reads as worthless one rank at a time and would never be bought into, so every reachable level competes on what the whole climb costs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Light's Potential grants 695 primary; Potion of Recklessness grants 1725 of the highest secondary and takes 232 off the lowest. Both are a third consumable axis, so picking both ranks them against each other in one run rather than needing two. Counted at a tenth of the fight. Both last 30 seconds on a five minute cooldown, and a flask is up the whole time - a potion counted in full would read as ten times the buff it is next to one. Since both potions share a duration and a cooldown, scaling doesn't tilt the comparison between them, only their size against everything else. Highest and lowest secondary here mean the character's own ratings, through getHighestStat and getLowestStat, which is what the in-game wording says. That is deliberately not the BEST_SECONDARY marker the enchants and food use, which follows the spec's stat weights: a potion reading "your highest secondary stat" takes whichever rating is largest whether or not the spec values it most. Worth knowing: this makes an Optimize Everything run three times bigger, since every consumable axis multiplies into the rest. A test that pinned the old count caught it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Searching two potions reported a 2.56% upgrade on gear that hadn't changed. Both potions weren't being used at once - the winning set carried exactly one. The baseline was the problem: it took the settings dropdown, usually None, while every candidate carried the better potion, so the potion's whole value was counted as a gear upgrade. The equipped set is now measured across the same consumables the run searches, so what's left is the difference the gear makes. Identical gear reports 0% whether one potion is pinned or both are searched. Same shape as re-gemming the baseline, and the same lesson: a comparison is only worth anything if both sides get the same options. Close alternatives now say which kind of decision they are. Consumables were being reported as enchants on a slot called "potion", and a weapon enchant swap appeared three times because the breakdown holds the weapon under three aliases. Both fixed, and flask, food and potion swaps now render as their own chips. Gem-only alternatives are capped at three and have to be worth at least 0.05%. Sets differing only in gems outnumber everything else by orders of magnitude and are mostly worth a hundredth of a percent, so they filled the list and buried the trinket, enchant and potion swaps worth reading. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
The threshold only covered alternatives that changed nothing but gems, so a set shuffling a gem and an enchant together went straight past it - and those were most of the list. It now applies to every alternative whatever it changes. The percentage it filters on was quantised and had to be fixed first. The score difference was rounded before being divided, so a set 14 healing behind and one 77 behind both came out as 0.00%: no use to read, and useless to filter on. Rounding now happens for display, where it belongs. Alternatives describing the same swap are also collapsed. The same enchant on either ring is one decision, and the list was showing several of these as separate rows. On a run searching three gems and two enchant slots, the list went from leading with 0.00% rows worth 14 healing, several of them duplicates, to nothing below 0.056% and no repeats. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
A gem alternative was a bare icon: it said what to buy but not where it goes, which on a set with four sockets is most of the question. Each swapped gem now carries the socket it would fill, assigned the same way the report hands gems to the item cards - in slot order, each socketed piece taking the next ones. Worth being plain about what that is: gem position isn't modelled anywhere. The engine picks one multiset for the whole set and any socket would take any of them, so this is an assignment rather than a recommendation. It is the same assignment shown on the gear, which is what makes it act-on-able. With the socket attached these are worth reading again, so the floor drops back to 0.01% and the cap of three gem-only rows is gone. The threshold still applies to every alternative whatever it changes, so the sub-0.01% shuffles that prompted the filter stay out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A partly upgraded set flatters everything compared against it: a piece can win its slot only because the gear beside it is three ranks short, which is a reason to spend crests rather than to chase loot. A checkbox now raises every equipped piece to the top of its own track first, answering the other question - what is still worth chasing once those crests are spent. Off by default, since "what beats my gear as it is" is the usual question, and remembered for the session like the other Upgrade Finder choices. Each track goes to its own cap rather than a shared one, the raising is done on copies so the player's gear is untouched, and a piece with no track is left alone - crafted pieces this season carry none, and there is nothing to raise them to. Fixes a bug this uncovered: clone() never carried enchantID, a field added later without updating it. Upgrades are scored on clones, so under "keep my gems and enchants" a raised piece silently lost its enchant and fell back to the automatic pick - which is not what it is wearing, and not what the baseline was scored with. The choice of baseline is its own function so the wiring is testable. Testing the raising alone left it uncovered: the engine could ignore the setting entirely and every test still passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Raising the player's gear was only half of it. Candidates were still offered at the level they drop at - one rank of six - so a finished Hero piece was measured against a fresh Myth one, and the number reported the crests not yet spent as much as the piece itself. That is the skew WoWAudit rejects the report for. With the option on, only the finished versions of a candidate are offered: "max", the top of the track it drops on, and "bonus", the top of the vault track above it. A 6/6 Hero piece and a 6/6 Myth one, which is the comparison worth making. The engine already built all three versions, so this is a choice of which to offer rather than new arithmetic. Both the raid and Mythic+ paths go through one function for that choice, including the dungeons whose end-of-run and vault share a track and so only ever had two versions to remove one from. The setting already travels with the report - shortenReport stores the whole Upgrade Finder settings object - so which mode generated a report can be read off the link without anything further. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
WoWAudit accepts a report only if it was generated a particular way. Most of its conditions the Upgrade Finder meets and could not fail to meet: there is no AoE or fight-style model to get wrong, no Power Infusion modelled at all, candidates are never granted a socket, and each is measured on its own against the baseline. Two are choices, and both have to be right or the report comes back rejected rather than wrong: the gear has to be fully upgraded on both sides, and the fight has to be five minutes. QE scores a raid at 6:40. The toggle sets both together instead of leaving the player to remember one of them, and prints what the report was run under on the report itself - a number on a page being no evidence of how it was produced. The stamp separates the conditions the toggle enforced from the ones that merely hold, because claiming to have checked something there is no check for is worse than saying nothing. Fight length reaches the scoring code through the character's own cast model, which the rest of the app shares, so the pin is put back in a finally: a run that throws must not leave the character scored at five minutes everywhere else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
The 6/6 comparison raises copies, deliberately - the player's own items are shown elsewhere in the app and saved to the character, so scoring must not rewrite them. But the uploaded report took its equipped list straight off the character, so a report scored at 6/6 declared gear at 1/6. Measured: the engine scored a 321 head while the report uploaded it at 308. That contradiction is what WoWAudit catches. It reads the equipped list to check the report was run with everything upgraded, and rejects it when the levels say otherwise - which is the rejection we have been getting, and it is not about fight length or any of the Droptimizer conditions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Crafted gear was already priced - one payment of 80 crests from its base level to its track's ceiling, 331 for Myth and 318 for Hero - but nothing could reach that price. The track dropdown offered only the five ladder tracks, so an item added by hand could never be marked crafted, and a piece with no track offers no upgrades and never appears in a plan at all. The 80 crests it costs were simply invisible. The dropdown now reads whether an item is crafted from the item database and offers the two crafted tracks instead of the ladder ones. Two things behind it were wrong in the same silent direction: a crafted piece sitting anywhere but exactly its base level matched no rank and offered nothing, and a plan reported the climb as starting from the base level rather than from where the piece actually is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
The engine always wore a flask - Automatic picked the best secondary, and there was no way to ask for none - so every Upgrade Finder score carried 165 of a stat regardless of what Top Gear was set to. It now takes a "None" flask, which Upgrade Finder always uses. It isn't offered in the settings panel; Top Gear scores exactly as before. The pinned-flask route needs no change of its own: Upgrade Finder already turns the detailed options off, and pinned consumables live behind them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
The engine already understood a "None" flask, and Upgrade Finder always uses it, but the dropdown only listed Automatic and the four stat flasks - so there was nothing to pick. It's now an option, and Top Gear wears no flask when it's chosen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gem, enchant, Omnium Folio and consumable selection for Top Gear, a sharded engine that can search all of it, crest spending plans, and an Upgrade Finder that measures against the gear you actually have — plus the SimC import, persistence and report work those sit on.
What changes for a profile that touches nothing
Most of this is opt-in, but not all of it. A reviewer comparing numbers against
devshould expect these to move:Top Gear
Gear options panel
A Detailed toggle beneath the item bar opens per-slot selection. Off, the engine ignores these settings entirely rather than just hiding them. Several picks in a slot expand the run into one variant per combination, ranked alongside every other set.
BEST_SECONDARYmarker so an enchant granting "your best secondary" resolves per spec rather than as a fixed stat.Keep my gems, enchants and runes
A top-level switch on the settings header. On, slots that already have a gem, enchant or rune keep it and the engine only decides the empty ones. Those slots also stop being searched, since every combination would score the same.
Optimize Everything
Also on the header. The engine searches every current gem, enchant, rune and consumable itself instead of you pinning each one, always at full depth. It supersedes the pins rather than overwriting them.
Search depth
The old hardcoded cap (12 gem loadouts in 24 variants) kept the first N combinations in build order rather than the best N. Depth is now a setting defaulting to No limit. An untouched profile expands nothing, so this only applies once something is multi-selected. The panel projects the real variant count before you run.
Item tools
Performance and scale
Large searches used to die mid-run with the progress bar frozen. Both memory limits are gone, and the run scales across cores.
TopSets). Results were all retained and sorted at the end, ~2.5KB each. Live heap growth over 355,806 evaluations: 876MB → 1MB, same winning set and differentials.forEachGearSet). Sets were all built before any was scored, ~790B each. Over 3,515,625 sets: 2,633MB → 18MB.Report
SimC import and persistence
omnium_talents. The four stat runes were mapped from one export per rune; the entry ids are a separate numbering from the rune spell ids.upgrade_currencies. Tier ids (3442–3446, Adventurer → Myth) were confirmed against a character's exact in-game totals.Crest spending
Its own toggle and settings section, off by default.
Upgrade Finder
WoWAudit has accepted reports generated with the no-flask and 6/6 changes. Which of those it actually checks is not known.
Also included
Earlier commits on the branch: crafted item and embellishment import fixes, every setting made reachable in the panel, Evoker mastery effectiveness, and absolute HPS reporting for specs that model it.
Testing
CI=true npx react-scripts test --watchAll=false— 65 suites / 656 tests passing, up from 309 ondevwhen this was opened. Production build clean.New tests were mutation-checked: the code each one guards was broken and the test confirmed to fail, and the ones that didn't fail were fixed.
Worth knowing for anyone working here:
react-scripts builddoes not type-check. An undefined component compiles cleanly and only fails in the browser, which is how a deleted component shipped mid-branch. The pinned TypeScript 3.9 can't parse the repo's own sources, sotsccan't catch it either. UI changes here are covered by tests that render the component.Known gaps
GemDB— only Telluric and Indecipherable are.Player.getFightLengthhardcodes 200s for dungeons.🤖 Generated with Claude Code
https://claude.ai/code/session_01E5STi454aLVN1cAsYyba3J