ItemDB-v1.7.0
What's new
Changelog
[v1.7.0] - 2026-10-20
Tests and tools: luacheck declared; gear EP audit tidy (tools only)
.writ-suites.jsondeclares aluachecksuite (luacheck .). Until now no suite ran it: the 44-suite gate on 2026-10-20 had no lint step for Lua at all..luacheckrcnow also excludestools/wago_cache, the git-ignored downloads (1,905 third-party files, ~679,000 warnings). The 45 repo files are all clean.Tests/gear_ep_audit.lua: the gear filter testedINVTYPE_NON_EQUIPtwice (an exact compare and a prefix match); only the prefix match remains (Peer Review 3988623c G4). The header now says that rows are scored with no options, so procs run at the flavour model's defaults, and what that misses (G5). G1's red-check was done with a real defect: removing TBC'swhiteDPSturned gear-ep-tbc red on exactly Hand of Justice's extra-attack row, and restoring it turned it green.Tests/sources_order_check.pydocstring: the "running build-sources-journal.py Cata" line it prints is the builder's own announcement before it calls the stand-in, not a rebuild. The check writes nothing.
Hand of Justice priced on TBC, Wrath and Mists; one whiteDPS definition (tools + scoring)
- Cause, traced in the client data: from TBC on, Hand of Justice's equip spell 15600 carries
SpellAuraOptions.ProcChance = 100(wago 2.5.6.68941, 3.4.3.58936, 4.4.2.60895, 5.5.4.68806), the "server decides" sentinelreal_chance()refuses; Vanilla's client says 2. The item has noitem_templaterate either, sotools/build-use-effects.pyfound no rate and listed it unscored. The world DBs do carry it:spell_proc_event15600ppmRate = 1in tbcmangos, wotlkmangos and mistsskyfire (deskitemdb-worlddb). The builder loaded that table already but read it only for on-use windows. It is now the LAST rate tier for an EQUIP proc with no other rate, so every row that already had a rate is unchanged. - Data: UseEffects rebuilt pinned (TBC 2.5.6.68941, Wrath 3.4.3.58936, Mists 5.5.4.68806, Vanilla
1.15.9.69722 then
split-seasonal.py Vanilla). HoJ shipsextra_attackatppm=1on all three. The same tier gave a world-DB rate to other equip procs that had none, HoJ included: proc rows Vanilla 191 -> 205, TBC 237 -> 254, Wrath 317 -> 335, Mists 376 -> 397, with unscored falling by the same 14 / 17 / 18 / 21 (file headers; the build log names each as aspell ppm <item>line). Those items gain EP. On Vanilla that is 14 items, not HoJ, which already hadch=2. Cata has no world DB of its own (SPELL_PPM_DBis Mists-only on purpose, Peer Review a7919e37), so HoJ stays unscored on Cata. whiteDPShas ONE definition, written in the library'sextra_attackbucket comment: the WEAPON DPS of one named raid two-hander, from the flavour's own core where it carries a DPS key and from WoWSims' database where it does not (Wrath), no attack-power term (a floor). Vanilla's 90 already was that (Corrupted Ashbringer 22691,DAMAGE_PER_SECOND=90in our core); only its comment said "the wearer's white DPS", now corrected. TBC declares 120 (Gorehowl 28773, 119.86) and Mists 4186 (Starshatter 86140, 4185.97), both first-tier weapons like Wrath's Naxxramas-25 223. Cata declares none: it ships noextra_attackrow. Pinned by the newTests/tbc_scoring_spec.luaand aTests/mists_scoring_spec.luacase. KNOWN COST: the tier rule is a first-tier raid weapon, and Vanilla is the exception: it keeps Naxxramas' 90, the last tier, because changing it would move live Vanilla scores (MELEE_HASTE_PCTand the weapon-skill default read the same number).
Mists mana procs priced for healers; gear EP audit reads ranged-AP twins (library + tools)
LibItemDB-1.0.luaproc scorer: a mana-return proc (b = "mana") read the raww[mp5]weight, so a flavour that DERIVES mp5 instead of weighting it scored every mana proc 0. Mists does exactly that (Scoring/Mists.luaprices mp5 as Spirit for healers), so the Idols of Wisdom, Price of Progress, Fathom-Brooch of the Tidewalker and the rest of 20 mana-proc rows scored nothing (gear-ep-mists run 20261020-102821.409-464835343). The mana branch now resolves its weight througheffectWeight, like the buff branch, and its breakdown shows the derived factors. EP CHANGE on Mists: those procs now score for healer specs; every other spec and every other flavour is unchanged (only Mists declares a derived mp5). Pinned by a newTests/libitemdb_spec.luablock. No operator ruling covers mana returns: pricing them rests on ItemDB's inference from the "healing has EP" ruling (directive 97bf22ed, which is about healing). If a ruling is wanted, the question is "should a mana-return proc have EP for healers?".Tests/gear_ep_audit.lua: on Cata and Mists a generic "+X attack power" effect ships as an AP row and an identical ranged-AP twin, and the twin is unpriced on purpose (the scales put a hunter's AP weight on melee AP; pricing both would pay a hunter twice,Scoring/Cata.lua"RANGED ATTACK POWER"). The audit failed all 54 / 52 twins; it now lists a twin (isRangedTwin: a melee-AP sibling on the same item with every field equal but the key and the amount) and still fails a lone ranged-AP row. Cata spell penetration is a documented zero there, asScoring/Cata.luaalready says. KNOWN COST: Cata 232012 Coren's Chilled Chromium Coaster ships melee AP 3576 beside ranged AP 4000 (Data/Cata/_core/UseEffects.lua:505; 71335, the same name, ships 4000 on both), so a Cata hunter is priced on 3576. Why the halves differ was not checked.- Comments:
Scoring/Mists.lua's socket block (Mists ships 17 cogwheel and 3 hydraulic gems) and mana-proc note,Scoring/Cata.lua's socket block, and the AP-pair note in the proc scorer (11817, 34472 and 34427 now ship the pair). tools/build-sets.pyclassify_procread only the first stat aura of a set-bonus proc, the same defectbuild-use-effects.pyhad (Peer Review 9ddcdb05 N3). It now returns every stat aura and the caller writes one row per aura with the same rate fields; Wrath's equal spell-power / healing pair is settled by the sharedsettle_heal_twin, which moved frombuild-use-effects.pyintoitemdb_commonso both builders use one copy. Every Sets.lua rebuilt pinned at its header build (Vanilla re-split): scored set procs TBC 35 -> 45, Wrath 39 -> 44, Cata 31 -> 36, Mists 34 -> 40, Forever 0 -> 0. The new rows are the second half of a two-stat proc -- on TBC, the ranged twin of an attack-power proc or the healing half of a "+damage and healing" proc (read from the TBC diff; the other versions' diffs were not read). The header now namesch|ppmandpm, which the rows carry (N4). EP CHANGE: set-bonus procs that buff two stats now price both halves as the scorer pairs them (AP max-paired; TBC healing and spell power priced per spec). Not checked in game.Tests/tarball_read_check.pygains an empty-regex case and amain()-level case (N5).
Tools only -- the gear EP audit's exceptions are tied to the scoring rules (Peer Review 3988623c, 737072573d5f, e750e48b)
Tests/gear_ep_audit.luasection 2 now prices each on-use / proc ROW alone (the item's lists are swapped for that one row while it is scored), so a priced row no longer hides a dead one on the same item; a part counts only when(part.ep or 0) ~= 0. The summary line prints the row count and the section title says ROWS.- Flavour-scoped zeros: Armor Ignored is excepted on TBC only, resilience on Cata and Mists only (TBC's scale weights resilience, so a TBC regression there now fails).
- On Cata and Mists the skipped keys are read from the rules actually registered (a copy, so the live rules are not mutated), not from a list kept in the audit.
- New
SCORER_KEYS: a key priced by a registeredlib.StatScorersfunction is never excepted, on any flavour --WEAPON_DAMAGE(weaponDamage),AMMO_DAMAGE(ammo), andWEAPON_SPEED/MELEE_WEAPON_SPEED(speed, registered by Mists only). Before this, Mists excepted both speed keys as "not a stat" althoughScoring/Mists.luaprices them, so a broken speed scorer would have passed. - New failure class, "stale exception": an excepted key that is priced anyway fails, so a
scorer that starts pricing an excepted key goes red until it is named in
SCORER_KEYS. That is what keeps the hand-kept map honest. - Red-checked 2026-10-20 by putting the defect in: with
speedremoved fromSCORER_KEYS, gear-ep-mists failed on exactlyWEAPON_SPEEDandMELEE_WEAPON_SPEEDas stale exceptions (run 20261020-132952.723-2215040992); restored, it passes (20261020-133101.345-1009074479).weaponDamagecould not serve: no shipped Cata gear carriesWEAPON_DAMAGE, so dropping it fails nothing. SCORER_KEYSgainssockets(everyEMPTY_SOCKET_*key the sockets scorer reads), so it matches the four registered scorers; the weapon-speed exception reason no longer calls speed "not a stat", which is false on Mists (Peer Review 70bfa528).isRangedTwinignores the amount on proc rows only; an on-use twin must match it too, since d and cd are all it carries beside the amount (Peer Review 9a3ffa52 N-A). gear-ep-cata 20261020-133316.577-1155008721, -tbc 20261020-133329.242-588795676 and -wrath 20261020-133554.172-941912331 pass with both changes.- Comments:
Scoring/Cata.luanames its one hydraulic-socket item, 60892, as hidden; the library'sGEM_SUBCLASS_SOCKETsays why one Cata/Mists map serves every flavour and that a Retail scoring file must make it per-flavour first.
School resistance under one key on Mists and Retail; Forever-only stats listed (tools + data)
- Gear EP audit finding: Mists and Retail cores carried a school resistance twice on one row --
ITEM_MOD_FIRE_RESISTANCE_SHORT=60besideRESISTANCE2_NAME=60-- soGetStatsreported it twice and anything summing resistances doubled it. The builders were already right (build-core.pycollapse_resist,build-core-wago.pyemitsRESISTANCEn_NAMEonly, and the Cata core was clean); the shipped data predated them. - New
tools/build-resist-collapse.py: applies build-core.py's owncollapse_resistto every row of a version's_coreand_seasonalclass files in place, reads no DB2, and callsvalidate_versionunconditionally. Needed because there is no Mists walk on this box to replay. Mists: 699 rows (948 duplicate keys dropped); Retail: 542 rows (671); Vanilla: 1 row (_seasonal/Armor.lua213299). A second run changes nothing. No re-stamp: the class files keep their walk build. Tests/equip_resist_check.py: no core row on any version may carryITEM_MOD_*_RESISTANCE*_SHORT. Red-checked against the real defect before the fix (Mists 948, Retail 671, Vanilla 1).- Forever's
EXPERTISE,ARMOR_PENETRATION,ITEM_MOD_ATTACK_POWER_VS_*_SHORTandITEM_MOD_SPELL_DAMAGE_VS_*_SHORTstay as they are and unpriced: Classic Era has no such stats forScoring/Vanilla.luato price (expertise and armour penetration are Forever combat ratings 24/25 in wow-ui-source-foreverPaperDollFrame.lua; the Era walk records no creature-type stat). Forever items' generic attack power beside the creature-type bonus is real (Beastslayer 11907: ItemSparse stat 38 next to stat 131).Tests/gear_ep_audit.lualists them as known exceptions;Tests/forever_vocab_check.pypins all three behaviours.gear-ep-forevernow passes with 0 unpriced (13 before). - Not verified: that Forever's armour penetration is a flat armour-ignored value.
- No resistance number was lost by the collapse (self-audit 2026-10-20 R1):
collapse_resistkeepsRESISTANCEn_NAMEand drops its twin without comparing them, so the newbuild-core.resist_conflictsnames a single-school pair that disagrees (the all-schools key is left out: "+10 all" beside a larger fire value is extra fire), andbuild-resist-collapse.pyprints each asCONFLICT.equip_resist_check.pypins the helper and reads every pre-collapse core row at commit a6bd125 through git: Mists 699, Retail 542 and Vanilla 1 rows carried both spellings, and every pair agreed (34/34). Those counts match the agent's own.
Cogwheel and hydraulic sockets priced; their gems no longer read as meta (tools + data + library)
- Gear EP audit finding:
EMPTY_SOCKET_COGWHEEL(Cata 7 items, Mists 15) andEMPTY_SOCKET_HYDRAULIC(Mists 30, e.g. 86226 Claws of Shek'zeer) priced 0 for every spec, because the socket scorer's colour map had only R/Y/B/M/prismatic. Worse,tools/build-gems.pywrote every gem with no red/yellow/blue Pawn flag as metaM, so cogwheels and Sha-Touched gems were counted as candidates for META sockets and inflated Cata/Mists meta-socket scores. LibItemDB-1.0.lua:EMPTY_SOCKET_COGWHEEL = "C",EMPTY_SOCKET_HYDRAULIC = "H"andGEM_SUBCLASS_SOCKET = { [9] = "H", [10] = "C" }(around line 2666); a gem is matched to its socket by its core subclass, so the library is right even against an old Gems.lua.tools/build-gems.py:SOCKET_SUBCLASSper version (Cata {10: C},Mists {9: H, 10: C}; per version because Retail numbers gem subclasses differently) andMETA_SUBCLASS = 6;colourless_kind()gives C/H/M from the subclass read out of the shipped_core/Gem.lua, and a colourless gem that fits no socket is skipped and listed on stderr instead of written asM(none did). Cata Gems.lua: 9 rows M->C (59477-59480, 59489, 59491, 59493, 59496, 68660). Mists: the same 9 plus 77540-77547 M->C and 89873, 89881, 89882 M->H; 60 meta, 17 cogwheel, 3 hydraulic. TBC and Wrath rebuilt byte-identical. Rebuilt throughitemdb-build(the builder takes no--wago-build; its only versioned input is the cached Pawn file).- Cata and Mists meta-socket scores DROP: the meta socket's best gem can no longer be a cogwheel.
- 98056 (Mists, subclass 9) has no gem row, and is now HIDDEN instead: it is an unobtainable
duplicate of the Sha-Touched gem Crystallized Horror 89882 (no loot, vendor or quest source in
SkyFire 5.4.8, absent from AllTheThings, same enchant 5115), so
tools/curated_hidden.pylists it andData/Mists/_core/Hidden.luacarries it. Search and the gem rankings no longer offer it. - Pinned in
Tests/gems_socket_check.py(new, declared in.writ-suites.json, 21 assertions; red-checked by emptyingSOCKET_SUBCLASS). Not verified: in game, and the shipped-file half of that check was not run against the old Gems.lua.
Stacking proc windows priced: Blackened Naaru Sliver, Death Knight's Anguish (tools + data + library)
- Gear EP audit finding: Blackened Naaru Sliver (34427) shipped
d=0 st=1on TBC, Wrath, Cata and Mists and priced 0. Its equip spell 45355 (10% on hit, 45 s cooldown) triggers 45040 Battle Trance (20 s, 100% on hit), which triggers 45041 Combat Insight (+44 AP and +44 ranged AP, CumulativeAura 10, no timer of its own).proc_effecttook the inner spell's 0 duration and the outer spell's stack cap. wowsims/wotlkmelee_trinkets.gomodels the same 20 s, 10-stack window. tools/build-use-effects.pyproc_window(): taken only when the middle spell has a duration and a 100% chance with the outer proc's scope, and the inner spell has no timer and a stack cap of 2 or more. One row per stat (both AP and ranged AP now ship), carrying the newwsfield.LibItemDB-1.0.lua:averageStacks()(a port of the builder'saverage_stacks, same 0.05 s step);scoreProcEffectsprices aws > 1row asm x averageStacks(trigger rate, d, ws) x uptime x weight, the trigger rate being the row's own (hitsPerSec). No world DB has a rate for the window, so none is baked in. README documentswsunderGetProcEffect; it ships in MINOR 46, which is unreleased.- The same shape moved Death Knight's Anguish (38212, +15 crit rating a stack) from
LoadUnscoredProcsto priced on Wrath, Cata and Mists. Nothing else moved in those four. Each UseEffects rebuilt pinned at its header's build (TBC 2.5.6.68941, Wrath 3.4.3.58936, Cata 4.4.2.60895, Mists 5.5.4.68806). Vanilla and Forever UseEffects were rebuilt pinned later, with the one-row-per-aura change below; neither gained awsrow (Peer Review 6b47813f item 3). That no Vanilla or Forever proc has the two-hop window shape is the likely reason, not verified. - Pinned in
Tests/use_window_check.py(red-checked by putting the defect back, which failed on the exact old row) and in a 7-testTests/libitemdb_spec.luablock, which also pins the cappedaverageStacks(0.5, 20, 10)= 4.791521286738454 (the builder's own figure, held in both ports) and the resulting ~187 EP (44 x 4.7915 x 20/45 x 2). The uncapped bound is ~196 EP (the spec's ws=100 fixture).gear-ep-tbc: 0 unpriced on-use/proc items.
One proc row per stat aura (tools + data)
tools/build-use-effects.pyproc_effectread only the first stat aura of a proc spell, so a spell buffing two stats shipped half. It now returns every stat aura and the main loop writes one row per aura, with the same rate fields. Shard of Contempt (34472) ships its attack power AND ranged attack power rows on TBC, Wrath, Cata and Mists; on Vanilla, 11817 gains its ranged twin and Felstriker (12590) gainsHIT_PCT=100besideCRIT_PCT=100(3 s). TBC and Wrath ship the same two rows (Data/TBC/_core/UseEffects.lua:208,Data/Wrath/_core/UseEffects.lua:290) and priceHIT_PCTlinearly as hit rating (Scoring/TBC.lua:136,Scoring/Wrath.lua:129). KNOWN COST on Vanilla, TBC and Wrath: +100% hit is far past the hit cap and the price is not capped, so Felstriker's hit half is overpriced; by how much is not measured. Whether those TBC/Wrath rows are new in this rebuild was not checked.- EP CHANGE for hunters (Peer Review 9ddcdb05 N1): 11817 on Vanilla, and 34472 / 34427 from TBC
on, now ship the attack power AND ranged attack power halves of one proc. The proc scorer
max-pairs the two (
apPairEp), so a hunter's score takes the larger half, where before it took the melee half alone on 11817. That 11817 shipped melee AP only before is from the builder's first-aura bug, not from a diff (not verified); the size of the move was not measured. - Wrath: a "+N spell power" spell carries a damage aura and an equal healing aura; the builder's
new
settle_heal_twin(Wrath only) drops the healing half when it equals the spell power and keeps only the excess. 47662 and 47671 (which had shipped the healing half alone under the first-aura bug) now shipITEM_MOD_SPELL_POWER=234.Scoring/Wrath.luaprices any healing row left as spell power 1:1 for healer specs only (effectWeightgainsonly =). - Every UseEffects file rebuilt pinned at its header build; Vanilla re-split with
split-seasonal.gear-ep-wrathpasses with 0 unpriced keys. Pinned inTests/use_window_check.pyandTests/wrath_scoring_spec.lua.
Attack-speed on-use trinkets priced (tools + data + scoring)
- Kiss of the Spider (22954) shipped in
LoadUnscoredProcsat 0 EP on Classic Era: its use spell 28866 is +20% melee (aura 138) and +20% ranged (aura 140) attack speed, andbuild-use-effects.pyread neither. The aura -> key map is nowitemdb_common.HASTE_AURA_KEYS, shared withbuild-aura-buffs.py(it was a privateHASTE_KEYin each builder): 138 ->MELEE_HASTE_PCT, 140 ->RANGED_HASTE_PCT, and WoW Forever's aura 319 (the same spell's melee half there) ->MELEE_HASTE_PCT. Two auras naming one key add, through the sharedhaste_fold. - Chance-on-hit haste procs:
proc_effectreturns every haste half of the spell (haste_by_key), and the main loop prices the half that speeds this weapon's own swing -- ranged haste on a ranged weapon (RANGED_INV), melee haste otherwise. Before, a ranged-haste proc was unscored while the same aura on-use was priced. Scoring/Vanilla.lua:DERIVED.MELEE_HASTE_PCTprices 1% asMODEL.whiteDPS / 100main-hand weapon DPS,family = "melee", so hunters take only the ranged half (their scales already weightRANGED_HASTE_PCT). An approximation, stated in the comment; a DERIVED row is fixed at load, so a caller'sopts.attack.weaponDPSdoes not reach it.Scoring/TBC.luaconverts both halves to haste rating like Wrath/Cata/Mists.- Charges-only on-use (Manual Crowd Pummeler 9449: 3 charges of +50% melee haste, no cooldown on
Classic Era and TBC) is now priced as one use per
CHARGE_USE_CD= 180 s, ItemDB's own figure, matching Forever's cooldown on the same item and Season of Discovery's 213407 casting the same spell. Positive charges only (negative charges mean a one-shot item that is deleted). KNOWN COST: three charges are three uses and the mace is kept once they are spent, so this prices a finite buff as if it were reusable without limit. - Gear that casts a consumable's own spell is unscored (
consumable copyin the build log): re-casting replaces the aura, so it adds nothing to the flask, elixir, food or drink a player already runs. A consumable is upkeep byItem.SubclassID(2 Elixir, 3 Flask, 5 Food & Drink) OR by itsItemEffect.SpellCategoryID(11 Item - Food, 59 Item - Drink, 79 Item - Potion (Non-Combat)) -- the category is needed because Classic Era's client files its consumables under subclass 0. Effect: 4696 Lapidis Tankard of Tidesippe (a drink's mana regen) is unscored on Vanilla, Forever and Mists (TBC, Wrath and Cata already left it unscored), and Forever 286541 Bael'dun Tankard likewise; Mists 81136 Cloak of Hidden Flasks (Flask of Falling Leaves) stays unscored. - Rebuilt pinned, every row against HEAD: Vanilla +22954, +19343 Scrolls of Blinding Light, +9449
priced, 4696 unscored, re-split through
split-seasonal.py; TBC +9449 (500 haste rating); Forever +22954 both halves, +9449, +19343, 4696 and 286541 unscored; Wrath +44096, an earlier fix never rebuilt into Wrath; Mists 4696 unscored; Cata unchanged. From TBC on, Kiss is +200 haste rating, which already shipped. Data/Mists/_core/AuraBuffs.lua(rebuilt at 5.5.4.68806 through the shared map): the five 10% melee-haste raid buffs (Cackling Howl, Serpent's Swiftness, Swiftblade's Cunning, Unholy Aura, Unleashed Rage) also carryRANGED_HASTE_PCT=10, which Mists' aura 342 grants. The ranged half is from the client data only, not seen in game. The generated header now names the haste keys each file's own rows carry (build-aura-buffs.py, Peer Review N2 and fd6f4b3b F3): Mists listsRANGED_HASTE_PCT, Wrath and Cata do not, because no row there has one. Wrath and Cata AuraBuffs were rebuilt pinned (3.4.3.58936, 4.4.2.60895) on 2026-10-19;git diffafter that per-file header rebuild is +6 -6 on each, all in the header comment, every row identical to HEAD (Peer Review N3).aura_buffs_checknow holds each modern header's key list equal to the*HASTE_PCTkeys its rows carry (Peer Review N4; red-checked by puttingRANGED_HASTE_PCTback into the Wrath header, which failed exactly that check, 1 of 74).Tests/libitemdb_spec.lua: four haste specs;Tests/use_window_check.pypins both halves of 22954, 9449's charge row, 4696 as a consumable copy, and every scored on-use whose buff lasts at least its cooldown (13143 Mark of the Dragon Lord on Cata/Mists, hidden 1912); the lib's no-premium comment now names 13143 instead of the unscored 81136.- EP CHANGE in
scoreUseEffects(LibItemDB-1.0.lua, Peer Review A6): a use buff that lasts at least its cooldown now scores with uptime 1 and premium 1 -- the use premium (the default, or a caller'spremium) is dropped, because a buff that never falls off is a flat stat, not a burst. Shipped effect: 13143 Mark of the Dragon Lord scores lower on Cata and Mists than in v1.6.x (+44 spirit, d=1800 on cd=1800); 1912 Deprecated Reed Pipe also qualifies but is hidden on every flavour. Tests/use_window_check.py(Peer Review J1/J3/J4): oneuse_effects_blockshelper splits each UseEffects.lua for all three readers; a guard counts every[id]=row and{k=entry in the LoadUseEffects block against whatROWparsed, so a row the regex misses fails instead of vanishing (red-checked: narrowingROW'sm=to integers failed it on Vanilla, TBC, Wrath and Forever); and the full-uptime check is renamedon-use rows (served and hidden) with d >= cd, because it reads hidden rows on purpose (1912).- Red-checked the Mists aura 342 ranged half (Peer Review D1): with 342 mapped to melee only and
Mists AuraBuffs rebuilt,
aura_buffs_checkfailed exactlyMists Unholy Aura 55610 is 10% melee AND 10% ranged haste(1 of 71); restored and rebuilt at 5.5.4.68806. - Re-checked against the data on 2026-10-19 (Peer Review D2):
use_window_check.pypins 13143 on Cata/Mists and 1912, which every flavour's Hidden.lua lists; Mists 81136 sits inLoadUnscoredProcs; and SoD 213407's ItemEffect row at 1.15.9.69722 casts spell 13494, the same spell as 9449's. - Also:
Data/Vanilla/_core/Effects.luaheld two rows joined on one line (4423, 4424), so its header stated 231 rows anddata_shrink_checkreported a loss. Regenerated withbuild-effects.pyat 1.15.9.69722 and re-split; it is byte-identical to HEAD again.
Developer test rings no longer ranked (data only)
- The "Indalamar's Ring of N stat" family (33986-33998, 34094, 34095, 34550, 34551; epic,
ilvl 130, no source -- guildorder.com's 34095 page: "dungeon journal does not list a source")
matched no
build-hidden.pyJUNK pattern, so rankings and BiS could serve them; 34095 ranked first by melee hit. New patternr"^Indalamar's "-- measured over every version's enUS names, it catches only that developer's test items (the rings, "Trinket of Pwnage", "Super Hot", "Debuffer", "Holy Hand Grenade"). Not confirmed unobtainable by an official source. - Hidden.lua rebuilt pinned at each file's stamped build: TBC +13 (4,730), Wrath +15 (6,320), Cata +20 (10,070), Mists +21 (13,458); the Indalamar's ids account for TBC +13, Wrath +15, Cata +15 and Mists +16. Vanilla, Forever and Retail ship no such names.
- SIDE EFFECT of the same rebuild, not of the pattern: Cata and Mists Hidden.lua each gained 5 AllTheThings rows (ATT count 3,665 -> 3,670 and 5,784 -> 5,789), so these items are no longer served there: 43670 Prophet's Enchanted Tiki, 43697 Artifact from the Nathrezim Homeworld, 43726 The Idle Crown of Anub'arak, 43821 Faceless One's Withered Brain, 43823 Head of Cyanigosa. Why ATT's signal moved was not checked; a newer installed AllTheThings is the likely cause, not verified.
Tests/hidden_patterns_check.py(new, declared in.writ-suites.json): real test names hidden, near-miss real names served, every shipped Indalamar's id in Hidden.lua. Red-checked by breaking the pattern (6 of 23 failed).- Forever carries the same family under another name: "Leafre's Ring of ..." (WoW Forever's
counterpart of the Indalamar's rings). New pattern
r"^Leafre's "inbuild-hidden.py; over Forever's enUS names it matches only those three rings. Forever Hidden.lua rebuilt at 1.60.1.69913, 1,664 -> 1,667. The three names are inhidden_patterns_check.py; red-checked by breaking the pattern (1 of 36 failed). Not confirmed unobtainable by an official source.
Hidden.lua records which AllTheThings it read (tools + data headers)
- Peer Review (thread f3669537, finding 2): the side effect above could not be explained from
the files, because the header stamped the build and the removed-with-patch cutoff but not the
ATT release.
att_hiddennow returns the source it read, andbuild-hidden.pystamps it as-- AllTheThings read: installed <toc ## Version>(orsource cache <sha>/not read (<why>)). The next ATT update shows as a changed stamp, separate from a JUNK change. - Every Hidden.lua rebuilt pinned at its stamped build so each carries the stamp (ATT 5.4.3).
TBC, Wrath, Cata, Mists and Vanilla: stamp line only (Vanilla re-run through
split-seasonal.py, 0 rows moved). Forever and Retail moved, by ATT rows only (the name and level counts did not change); that their last build read an older ATT is the likely cause, not verified, since nothing recorded it then: Forever 1,664 -> 1,663 (191481 Tabard of Mastery no longer hidden); Retail 13,923 -> 13,927 (+25 hidden, among them the same five 4367x-4382x items as Cata/Mists, 144373-144379 and 151221-151264; -21 served again: 185784-185785, 250293, 17 ids in 255473-255491, 268551). Not checked item by item against the game. - Forever's drop is now confirmed and fixed (Peer Review fd6f4b3b): installed ATT 5.4.3's
db/Camelotno longer mentions 191481 at all, and that entry was its only Forever hidden flag. The Tabard of Mastery is a Season of Mastery reward (ATT u=16) and already hidden on Vanilla, Wrath, Cata and Mists. That it is unobtainable on Forever rests on the older ATT build having flagged it there (HEAD's Forever header counted it as ATT); it was not checked in game. It is now intools/curated_hidden.pyMANUAL_HIDDEN. Forever Hidden.lua rebuilt pinned at 1.60.1.69913: 1,664 again (5 ATT, 5 hand-curated);data_shrink_checkandhidden_patterns_check(33/33 at the time) green. tools/build-hidden.pynow logs every ATT id it does not count, split into "not in this version's names" and "already hidden by name or RequiredLevel" (Peer Review 6b47813f item 5). Forever's sixth ATT id is 4116, which Forever does not ship; that rebuild was row-identical (1,664). The shipped Forever Hidden.lua now holds 1,667 (1,639 by name): a later rebuild added the three Leafre's test rings (274978, 276765, 285326), hidden by name (Peer Review ca334b59 F3).Tests/hidden_patterns_check.py's data half widened from the Indalamar's family to every name ANY JUNK pattern matches, on every version with a Hidden.lua (33 checks). Red-checked with a real defect, an added^Linen Cloth$pattern no file was rebuilt with: 7 of 33 failed. The three Leafre's names (above, added later) took the check to 36.- Self-audit: a non-zero
att_extract.luaexit is now stamped (, extract exit N) rather than read as a clean ATT read. The ATT cache paths were spelled in three files (build-hidden.py,build-item-sources.py,fetch-att.py); the two builders now importATT_CACHE_DIR/ATT_CACHE_METAfromitemdb_common, andTests/itemdb_common_check.pyholds the import-freefetch-att.pyto them (red-checked: renaming the shared pin file fails it).
Seasonal test items hidden again on Classic Era (data + tools)
- Peer Review (thread 502f816e):
build-hidden.pyandhidden_patterns_check.pyboth calledread_names(version, "enUS")withoutinclude_seasonal, so neither saw the namessplit-seasonal.pyhad moved toData/Vanilla/_seasonal/enUS/Names.lua. The shipped Vanilla Hidden.lua had been built after a split, so seasonal test and placeholder items (209526 TEST Darkwater Talwar, 213391 UNUSED Electrified Mithril Gauntlets, 215235 2hander Animation as 1hander test copy, ...) were served on Season of Discovery and Fresh Anniversary realms. Both now passinclude_seasonal=True, so the builder's output no longer depends on whether it runs before or after the split. Data/Vanilla/_core/Hidden.luarebuilt pinned at 1.15.9.69722: 2,110 -> 2,137 (+25 by name, +2 by RequiredLevel > 60; ATT unchanged at 417). Red-check: the widened check failed on exactly these rows before the rebuild, then passed 33/33 after it.- Peer Review (502f816e P1): the same defect was in ten more builders.
Tests/seasonal_ids_check.pyalready requiredinclude_seasonal=True, but judged only builders some_seasonaloverlay header names -- and a builder that never saw a seasonal id never made an overlay, so it was never judged. The check now judges everytools/build-*.py; seven areEXEMPTwith a written reason (build-sources, -imenso, -journal, -wowsims, build-bis, build-bis-classic, build-rep-loot: their tables are in split-seasonal'sskipset, so a seasonal row would load on Era unguarded). Red-checked by dropping build-hidden's flag: the check namedbuild-hidden.py:363.build-expansions.pyis EXEMPT too, found by the self-audit after I had first "fixed" it: Expansion.lua is grouped ([-1] = "id,id,..."), not one[id]=row per item, so split-seasonal cannot partition it, and reading seasonal ids put 6,522 of them in the BASE file as expansion -1 "not known", loading on Era. Reverted and rebuilt (17,605, all expansion 0). - Peer Review (ab446d2d P1): the exemptions are now checked, not just written.
split-seasonal's skip set is the module constant
SKIP(Expansion added: it is keyed by expansion, not item id).seasonal_ids_checknow asserts three things. Every table an EXEMPT builder writes in a split version, read from the_coreheaders, is inSKIP. No EXEMPT builder passesinclude_seasonal, which is the guard that was missing for the Expansion regression. And every table split does partition is[id]=or["key"]=rows at column 0. Building it found two false reasons: build-sources-imenso and -wowsims write only Forever (which has no_seasonal), notskip-set tables. Their reasons now say so, and the check holds them to it. Red runs: with RepLoot dropped fromSKIP, the check named RepLoot. Re-adding the Expansion flag and rebuilding went red on all three new pins, the shape pin namingExpansion.lua:15 [-1] = "...". Reverted and rebuilt; split then moved 0. 48/48. - Self-audit (a88dc308 F1): an EXEMPT builder calling
shipped_ids()(always base + overlay) is now refused too. Red-checked by switchingbuild-expansions.py:75to it. - Peer Review (a88dc308 P2):
core_rowsis now a judged gate besidecore_idsandread_names. It has the sameinclude_seasonal=Falsedefault, and nothing was looking at it. The gate also reads a positional flag (core_ids(v, True)), per Peer Review N1. The first run went red onbuild-tokens.py:224and onbuild-core-wago.py:398/:588andbuild-core-wowsims.py:364. build-tokens now passes the flag, since TokenRewards is partitioned. Vanilla was rebuilt and is unchanged (87 tokens, 250 rewards); split moved 0. The other three calls sit in verify/report functions (read_core,verify_structure,verify_armor). Those are exempted by function, not by builder, inREPORT_ONLY, and each is held to making no write call.derive-core-maps.pyis not judged: it writes no Data file. - Peer Review (a88dc308 F2): two pins that the split moves only item rows. First, every Load
table in a partitioned base file whose keys are mostly not shipped item ids must be named in
NON_ITEM_TABLES, and all its keys must sit belowTHRESHOLD. Today that is only Reagents.lua's skill-line table, which split-seasonal's own docstring said stayed in the base "by position, not design". Second, no base TokenRewards row may name a seasonal-only reward, because split moves a row by its token key. Red runs: unlisting Reagents#2 named it, and adding 202254 to token 5824's rewards namedTokenRewards.lua:15. Both reverted. Self-audit: the table-opener regex first knew onlylib:LoadX({, so the 13lib:LoadCore(n, {class files were never judged. Now it also matcheslib:LoadX(<args>, {, and a pin refuses any opener it does not recognise. Red-checked with the old regex: all 13 class files named. 59/59. - Peer Review (a88dc308 L1, L2). The no-write pin now also knows json
dump, the shutil copy family,move/rename/unlink/mkdir/touch, andprint(file=...)to anything but stdout/stderr. Barecopyandreplaceare left out, becausedict.copyandstr.replaceshare the names. An item-keyed table may no longer carry an unshipped key >= THRESHOLD, since split would move that row by a meaningless key. Red runs: a plantedwrite_textinread_corenamedwrite_text@408, and a planted[299999]row in Effects.lua namedEffects.lua#1. Both reverted. 59/59. - Self-audit 6633cdd6 F4, which Peer Review agreed was latent: the
REPORT_ONLYexemption now also checks every place the function is called. A call passes when it sits in another report-only orverify*function, when its result is thrown away (main'sverify_structure(v, b)), or when it is inside anifwhose body ends insys.exit(main'sif args.verify:branch forverify_armor). Any other call is named, because the base-only answer could then reach a write. The two allowances based on names or code shape are checked too. Averify*caller's whole body must make no write, and an allowed exit branch must make no write before itssys.exit. Red runs:_planted = verify_structure(v, b)in main namedmain@633, and awrite_textplanted in build-core-wowsims'--verifybranch namedexit branch@477:write_text@482. Both reverted. 68/68. - Peer Review (4797cfd8 W1-W3). Every no-write pin uses a write set worked out from the code,
not a fixed list. Any function in
itemdb_common.pyor the builder whose body writes, directly or through another such function, counts as a write. That brings in ItemDB's own Data writers,write_names,write_sourceandwrite_spell_schools, which the old list missed.open()now counts only in a write mode, sop.open("rb")is a read. Writes to the download cache are excluded when the target isCACHE_DIR, or a variable built from it or from a*_cache_path()helper. Without that,wago_csv's cache write made every DB2 reader look like a writer. New pins hold that exclusion honest:CACHE_DIRmust sit undertools/, and every*_cache_pathhelper must build on it.shutil.copyandos.replace/os.rename/os.removeare now recognised, whilePath(...).replacestays the known gap (W2). A comment records that the three report-only functions fill no container they are passed (W3). Red runs:write_sourceplanted inverify_structureand in build-core-wowsims'--verifybranch, plusshutil.copyinread_core. All three were named, and the copy also turnedread_coreinto a writer, which flagged itsverifycaller. All reverted. 72/72. Peer Review's follow-up note:extractall,urlretrieveandrmtreenow count as writes too. Red run: anextractallplanted in the--verifybranch namedextractall@482. Reverted. - Fixed: build-consumable-groups, -droprates, -factions, -gems, -item-places (both
its
core_idsand the gathering-noderead_names), -mounts, -races, -reagents, -spell-schools, -use-effects. Vanilla rebuilt with each, thensplit-seasonal.py: 1,259 seasonal rows into seven NEW overlays, now listed inItemDB.toc--_seasonal/Factions.lua517,UseEffects.lua481,SpellSchools.lua178,Reagents.lua31,Hidden.lua27 (the 27 above, moved),Mounts.lua18,Races.lua7. So on SoD / Fresh realms, seasonal items now have their faction and race limits, on-use and proc effects, spell school, reagent use and mount flag, none of which they had before. ConsumableGroups, DropRates and ItemPlaces moved no rows; all three are keyed[id]=(read), so a seasonal row would have been partitioned. That their sources (WoWSims, CMaNGOS) carry no seasonal item is the likely reason, not verified. Base files unchanged in count (data_shrink_check: no base shrink). None of it is checked in game. hidden_patterns_check.pyreads_seasonal/Hidden.luaas well as_core.
On-equip spell stats one low on Classic Era and TBC gear (data only)
- Operator, 2026-10-18: Hand of Justice (11815) is +20 Attack Power in game; the core shipped 19.
Root cause: the Classic Era and TBC clients'
GetItemStatsreturns an on-equip aura's rawEffectBasePointsand leaves out theEffectDieSides(1) the tooltip adds, and the walk recordsGetItemStats. Seasonal items carry DieSides 0, so their walk value was already right; the Mists and Forever walks already report the tooltip value (Drake Fang Talisman 19406: 56 there, 55 here). tools/itemdb_common.py:equip_aura_rows(raw base + DieSides of every on-equip APPLY_AURA) andequip_die_corrections, which raises a key by its auras' summed DieSides ONLY when the walk's value equals their summed raw base -- so a corrected core, a seasonal item and a client that already reports the tooltip value come back unchanged.EQUIP_DIE_KEYS(aura -> walk key) was derived from the data, not from names: AP 99/124, spell damage 13 (all schools and per school), healing 135, mp5 85, health regen 161, equip-spell school resistances 22, and TBC's aura-189 ratings (defence, dodge, parry, block, hit, spell hit, melee/ranged crit, spell crit). Armour via aura 22 matched 2 of 12 items and is left out.tools/build-equip-spell-fix.py(new; deskitemdb-buildbuilderequip-spell-fix) applies it to the shipped class files in place, by item id, without replaying the walk. Keys raised, Vanilla at 1.15.9.69722: AP 596, melee AP 31, ranged AP 74, spell power 1,794, spell damage 378, healing 632, mp5 513, health regen 38, resistances 74. TBC at 2.5.6.68941: the same families (AP 1,487, spell power 2,315, spell damage 1,458, healing 1,181, mp5 1,180, ...) plus 1,544 rating keys. A second run changes nothing.build-core.pyapplies the same correction to every new walk.- KNOWN: the 15 TBC items whose walk folds melee and spell crit into one
ITEM_MOD_CRIT_RATINGfail the summed-base proof and stay one low. Wrath's on-equip stats: see the next section. Tests/equip_spell_stats_check.py(new, declared in.writ-suites.json): nothing left at the raw base, 4,879 Vanilla and 9,413 TBC keys at base + DieSides, and pins for 11815 (+20, the operator's tooltip) and 19406's TBC hit 20 / dodge 12 / AP 56 against the Mists walk. It was red on 11815 before the fix.- Peer Review 868b8260: the proof runs per row (a base and a seasonal row are proved separately;
0 ids carry both today, so no data moved), and
parse_blob/apply_blobinitemdb_commonare the one blob rewrite,apply_blobraising on a delta it could not apply. - Peer Review de472a90 finding 1:
EQUIP_DIE_KEYS'(13, None)row also caught aura 13 misc 1 (PHYSICAL damage), so TBC Bronze Band of Force 30804 -- 13/126 and 135/126 at 5 + 1 beside a 13/1 at 0 + 1 -- was lifted by 2 toSPELL_POWER7; the tooltip is 6.(13, 1)now maps toSPELL_DAMAGE_DONEonly, which is howGetItemStatsreports it (Might of Cenarius 21189, whose only aura is 13/1 at 3 + 1: TBC walk 3, Mists walk 4).equip_die_keysis the lookup (an exact entry wins), andequip_die_overliftundoes what the old lookup lifted; the fix tool ran it on TBC (30804 7 -> 6, the only row) and Vanilla (none). The check pins 30804 and 21189 and asserts nothing is left over-lifted.
Wrath on-equip spell stats (data)
Data/Wrath/_coreshipped every on-equip aura stat EMPTY: Hand of Justice 11815 and Drake Fang Talisman 19406 had no stats at all.build-core-wago.pybuilds Wrath from ItemSparse, which holds only an item's native stats; attack power, spell power, healing, mp5, health regen, armour and ratings on Classic gear are on-equip auras (ItemEffect -> SpellEffect).build-core-wago.pynow decodes them under the classic profile (aura_stats,item_auras): the amount is base + DieSides (the tooltip); attack power and TBC ratings become ItemSparse stat slots sofold_ratingsreports them exactly as stored ones; Wrath ratings take one unified key per group. The TBC walk gate holds it: 0 unexplained over 30,031 items, with 229 rows explained as the walk's own raw-base omission (see the previous section).- Wrath rebuilt at 3.4.3.58936: auras decoded on 9,237 items, of which 9,231 ship -- the other six (209528-209533) have no ItemSparse row, so nothing is written for them (the builder now prints them); 24,359 items with stats. 11815 AP 20; 19406 AP 56 / hit 20 / dodge 12; 21189 physical damage 4; 30804 spell power 6 plus physical damage 1 -- each equal to the Mists walk, which reports the physical part as spell damage (see the next section).
WALK_DRIFT['TBC']gains 34702: 2.5.5 ItemSparse armour 74, TBC walk 78, Wrath ItemSparse 78.- Aura 123 misc 1 (ignore N armour) is decoded as
ITEM_MOD_IGNORE_ARMORfor every version (see the spell-penetration section). No Wrath item carries it: 3.x converted those items to armour penetration rating, which Wrath ItemSparse already holds (904 items). Tests/core_wago_check.pycompares the shipped core against ItemSparse PLUS the auras, asserts the decode found more than 5,000 items, and pins the five items above.- Cata is built from the modern profile, whose ItemSparse already reports these stats (its 19406 carries AP 56); the aura decode is off there and Cata was not rebuilt.
The two on-equip aura decodes are held to agree (tools only, Peer Review de472a90 finding 5)
itemdb_common.EQUIP_DIE_KEYS(the walk keys an aura is reported under) andbuild-core-wago.py'sAURA_AP/AURA_KEY/RESIST_AURA/TARGET_RESIST_AURA/CR_BIT_STAT/ 13-135 pairing (the Wrath decode) say the same thing in two shapes; the wago one folds ratings and attack power through ItemSparse stat slots, so it cannot simply read the other table.Tests/aura_decode_agree_check.py(new, declared in.writ-suites.json) decodes everyEQUIP_DIE_KEYSentry as a one-aura item through the realsparse_statsand requires the keys to be ones that table names (or theirEQUIP_STORED_KEYrespelling) atequip_amount. What one side knows and the other deliberately does not is listed exactly: aura 22's magic schools (not decoded by the classic profile), 22's armour bit (not in the lift), and aura-189 bits 17-19 / 23-24 (no single-aura walk evidence). 42 checks; red on a plantedAURA_KEY161 respelling.
Spell penetration one high, and armour-ignore stored as spell penetration (Peer Review de472a90 finding 4)
- Spell penetration is aura 123 (MOD_TARGET_RESISTANCE) with a NEGATIVE base. The Classic Era and
TBC
GetItemStatsreports it negated and, like the positive auras, without the die: walk-(base), tooltip-(base + DieSides), so every walk value was one HIGH. Verified on three wowhead tooltips, 2026-10-18: Field Marshal's Silk Spaulders 16444 walk 11, "by 10"; Doomcaller's Robes 21334 walk 21, "by 20"; Shady Dealer's Pantaloons 30898 walk 176, "ignore 175".equip_die_correctionscould not see it: it summed the raw base (-11) against the walk's 11. itemdb_common.EQUIP_AURA_SIGN({123: -1}) andequip_amountmake the lift, the respell proof, the fix tool's derivation andbuild-core-wago.py's decode read the aura as positive.EQUIP_DIE_KEYSgains(123, None)->SPELL_PENETRATION.- Aura 123 misc 1 (PHYSICAL) is "your attacks ignore N of your opponent's armor", which the TBC
walk also reports as
SPELL_PENETRATION, so casters' scales priced a rogue's pants. It is now stored asITEM_MOD_IGNORE_ARMOR(ItemDB's own spelling:F:\Blizzard API Docs\GlobalStrings\enUS.luahas onlyITEM_MOD_ARMOR_PENETRATION_RATINGandITEM_MOD_EXTRA_ARMOR, and a rating is a different stat -- Peer Review 6e9ed265 F2) throughEQUIP_STORED_KEY, andphysical_damage_renames/physical_damage_mixedare renamedstored_key_renames/stored_key_mixedbecause they now cover both. - Data: TBC 192 rows lowered by 1 and 138 respelled; Vanilla 35 lowered (30 base, 5 seasonal; no
Vanilla item carries misc 1).
build-equip-stats.pyemitted misc 1 asSPELL_PENETRATIONtoo -- andGetStatsfills a key the core lacks from EquipStats, so 30898 would have carried both keys after the respell -- it now emitsITEM_MOD_IGNORE_ARMOR; TBCEquipStats.luarebuilt at 2.5.6.68941 (138 rows). Wrath rebuilt; the TBC gate's walk-raw-base explanations fell from 229 to 37 andSPELL_PENETRATION(54) andIGNORE_ARMOR(138) are exact. - Scoring:
Scoring/TBC.lualistsITEM_MOD_IGNORE_ARMOR("Armor Ignored") with no shipped weight. KNOWN COST: it prices 0. HawsJon's TBCArmorPenetrationmapped onto it gave Arms 1.1 EP per point (175 ignored ~= 430 AP), which I could not confirm is a per-flat-armour unit, so the mapping was reverted andBiSWeights.luais unchanged. So Arms and Fury scoring prices Shady Dealer's Pantaloons 30898's 175 armour ignored at 0 until a weight with a confirmed unit exists. build-equip-stats.pydecodes aura 123 throughitemdb_common.equip_amountwith the row's ownEffectDieSidesinstead of a hard-codedbase + 1(Peer Review 6e9ed265 F5), so the sign rule has one copy. Vanilla and TBCEquipStats.luarebuilt at their pins: no row moved.- Pins:
equip_spell_stats_check(51): 16444 / 21334 at 10 / 20 on Vanilla and TBC, 30898IGNORE_ARMOR175 and no spell penetration, the negative-base lift on 16444's real rows (11 -> -1), the respell on 30898's, and 30804's real rows as the live witness of a MIXED key (13/1 beside 13/126, counted bystored_key_mixedand left as walked bystored_key_renames, Peer Review 6e9ed265 F4); red on 8 before the data fix.core_wago_check(59): Wrath 16444 at 10.libitemdb_spec: a TBC fire mage's spell-penetration weight pricesSPELL_PENETRATION175 and notIGNORE_ARMOR175. - The TBC gate's walk-raw-base explanation is a value coincidence, so it now applies only to the
37 (item, key) pairs checked in at
tools/walk_exceptions.json(build-core-wago.RAW_BASE); a new match fails the gate and shows as a diff of that file (Peer Review de472a90 F6).core_wago_check(61) holds the observed set equal to the file both ways; red on the empty list. It also refuses a pair written twice (the json loads into a set, which would hide it), andexplain()'s docstring says the raw-base test runs first, so a mismatch another reason would cover must still be listed (Peer Review 2f81f49d N1, N2). WALK_DRIFT(the 12 TBC items whose armour the walk and every cached ItemSparse disagree on) moved out ofbuild-core-wago.pyinto the same file, with its explanation, so both per-item exception lists live in one place in one form.verify()now reports the drift ids it used, andcore_wago_checkholds them equal to the file both ways, where it used to check only that the count did not exceed the list -- a listed item that stopped drifting went unnoticed. Red on a bogus listed id.aura_decode_agree_check(51) counts resilience bits 14-16 among the unproved rating bits, and sweeps every aura id 0..511 against 27 masks: anything that decodes must be an auraEQUIP_DIE_KEYSknows, and each any-misc entry must agree on every mask it decodes (162 pairs), not one representative (Peer Review 7fb1a54b). Red on a planted constant-free aura-200 branch and a planted (161, 4) respelling. No unexpected hit.aura_decode_agree_check(54) now also holds the aura-189 masksEQUIP_DIE_KEYSdoes not name (13 in the sweep): each may land only on the keys of the named masks it shares a bit with. Two exceptions are listed exactly: a single hit bit (32 or 64) folds toHIT_MELEE_RATING/HIT_RANGED_RATING, where the walk evidence (mask 96) isHIT_RATING. No TBC walk item carries either split hit key, so neither side has evidence against the other, andScoring/TBC.luaaliases both ontoHIT_RATING. The masks no named mask covers must be exactly the unproved rating bits. Red onCR_BIT_STATbit 9 pointed at spell crit.- That sweep only held single bits, so it now also decodes every aura-189 mask an on-equip spell
really carries in the cached TBC and Wrath data (13 masks; the unnamed multi-bit ones are exactly
224 and 1792, pinned). 1792 (all three crits, Wrath item 867) folds to
CRIT_RATING, a key neither named crit mask names; the TBC walk itself reports that fold for a 768 and a 1024 aura at one amount (16577, 29598, ...). It is listed by hand asFOLD_189and asserted exactly, rather than allowed by calling the decode under test (Peer Review b7dc73db S1, 4b920cc8). Red on 1792 before it was listed. (54 -> 57 checks.) - Item 34095 is
HIT_RATING100 in the TBC core and melee 200 / ranged 100 in Wrath because the item changed, not the decode: the test ring has no aura, and 2.5.5's row is melee 100 + ranged 100 ("Indalamar's Ring of 100 Hit Rating") where 3.4.3's is 200 / 100 ("...of 200 Hit Rating").core_wago_check(63) pins both rows (Peer Review b7dc73db S2).
Physical damage no longer priced as spell damage (todo #8, Peer Review a93de54c)
- "+N physical damage" (aura 13 misc 1, Might of Cenarius 21189 "+4 damage") is reported by
GetItemStatsasITEM_MOD_SPELL_DAMAGE_DONE, so the Vanilla and TBC walk cores stored it there and every scale priced a melee ring as spell damage. My Wrath rebuild above did the same, andScoring/Wrath.luaaliases that key to spell power. - The cores now store it as
ITEM_MOD_PHYSICAL_DAMAGE_DONE_SHORT, the spelling the Forever walk already uses.itemdb_common.EQUIP_STORED_KEYis the one table (walk key -> stored key);stored_key_renamesrespells a key only when every aura reporting under it is a 13/1 aura and the value equals their summed base + DieSides, the same proof shape as the DieSides lift, so an item mixing physical and school damage under one number is left as walked. Newstored_key_mixedcounts those, and the fix tool andequip_spell_stats_checkprint the count: 0 in every Vanilla and TBC scope today (Peer Review 0c2a1e34 finding 3). Newrename_blobraises on a missing or colliding key. - Applied by
build-equip-spell-fix.py(after the lift) and bybuild-core.py'slift_equip_dies: TBC 4 rows (21189, 21766, 24114, 32654; 30804 has no spell-damage key, its physical aura's raw base is 0), Vanilla 20 (21189 and 19 seasonal items).build-core-wago.pyemits the stored key directly for every version and compares it in the TBC gate (4 exact, 30804's 1 explained as walk-raw-base, 0 unexplained); Wrath ships it on 5 items, and its spell-damage-only rows (97) are school damage. - Scoring: Vanilla, TBC and Wrath list it in
weightStats("Physical Damage") with no shipped weight, so it prices 0 and no breakdown calls it unknown. Its value scales with weapon speed, so a flat weight would be wrong; a custom scale can still set one. The stale "one legacy item" comment inScoring/Wrath.luais corrected. - Mists (Peer Review 0c2a1e34 finding 4): an earlier line here said the Mists wago schema has no
EffectBasePoints, so the proof could not run. That was wrong --SpellEffectat 5.5.4.68806 carries it (deskitemdb-schema); the Mists core had simply been built before the rename existed.build-equip-spell-fix.py Mistsrespelled 5 rows (21189, 21766, 24114, 32654 and 30804's physical 1, which Wrath's DB2 decode also gives), matching the TBC core by item id; 185987's spell damage 20 is a real school aura and stays. It also lifted 4 seasonal ranged-AP rows (185985, 186069, 199722, 209803) by 1, to the TBC walk's and Wrath decode's value: Mists' walk reports aura 124 at the raw base, unlike the other auras.Scoring/Mists.lualists "Physical Damage" inweightStatslike the other three. Pins:equip_spell_stats_checknow covers Mists (21189, 32654, 30804, 185987, 185985; red-checked by putting 21189's old spelling back), andmists_scoring_specholds 21189's 4 at 0 with no unpriced row (red-checked by removing the vocabulary row). In-game tooltips not checked. - Pins:
equip_spell_stats_check(40): 21189 physical 4 and no spell damage on Vanilla and TBC, nothing left to respell,rename_blob, the proof on 21189's real rows (4 respelled, raw 3 not), andbuild-core.lift_equip_diesturning a raw walk record's 3 into physical 4 -- red-checked by removing the rename fromlift_equip_dies.core_wago_check(58): Wrath 21189 physical 4 / no spell damage, 30804 spell power 6 / physical 1.wrath_scoring_spec: 21189's 4 scores 0 for a holy priest with no unpriced row, where the walk spelling scores 4.
Melee + ranged rating twins counted once in scoring (Peer Review de472a90 X1)
- One aura improving melee AND ranged crit (TBC aura 189 misc 768) is reported under both
ITEM_MOD_CRIT_MELEE_RATINGandITEM_MOD_CRIT_RANGED_RATING, and every flavour's rules alias both ontoITEM_MOD_CRIT_RATING.scoreStatssummed them, so Gloves of Holy Might 867 (14 and 14 for one +14 line) scored as 28 crit rating. At least 391 TBC core rows carry such a pair (Peer Review's lower bound from a text search, not counted by me). LibItemDB-1.0.luasettleTwins: crit, hit and haste melee/ranged pairs that alias to the same key now count once, by attack power's sign rule (apPairEp: same sign takes the larger, opposite signs sum). A lone side and the generic key are untouched. This LOWERS the EP of every such item -- a correction, not a regression.Tests/wrath_scoring_spec.lua: three specs (each twin family, the sign rule, one breakdown row). Red-checked: with thesettleTwinscall removed exactly these three fail (947 passed, 3 failed). A fourth pins an unequal pair (12 / 20) as ONE breakdown row at 20: rows are keyed by the alias (ITEM_MOD_CRIT_RATING), so the melee-or-ranged spellingsettleTwinskeeps (Peer Review a93de54c F7, now the sideapPairEpchose) never reaches a label.- KNOWN COST (Peer Review de472a90): an item with a melee-only crit aura AND a both-types crit aura reports melee greater than ranged; settling keeps only the larger, so the ranged-only part is dropped for a hunter. Whether any shipped item has that shape is not verified.
Hand of Justice's extra attack priced in EP (MINOR 45 -> 46)
- Operator, 2026-10-18: Hand of Justice (11815) showed only its Attack Power in Dibs' EP and
nothing for the 2% extra attack.
tools/build-use-effects.pypriced an extra attack as one swing of the item's OWN weapon damage, so a non-weapon had nothing to price with and was filed inLoadUnscoredProcs. It now ships{b="extra_attack", m=<swings>}for any item with no weapon damage in the client data; a weapon with damage (Ironfoe) keeps itsdamagerow. The Wrath heirloom Battleworn Thrash Blade (44096) is a weapon yet shipsextra_attack(Data/Wrath/_core/UseEffects.lua:445), so the builder found no damage for it; that a scaling heirloom carries none in ItemSparse is the likely reason, not verified. LibItemDB-1.0.luascoreProcEffects: newextra_attackbucket (breakdown keyPROC_EXTRA_ATTACK, label "Extra attack proc"): extra swings per swing (rate / swing rate x m, i.e. ch% x m) x white DPS x theDPS_MAINHANDweight only, since extra attacks are melee. White DPS isopts.attack.weaponDPS, else the newmodel.whiteDPS.Scoring/Vanilla.luadeclares 90, the figure its weapon-skill scorer already assumed (WS_DEFAULT_DPSnow reads it, one constant). Arms warrior: 0.02 x 90 x 5.31 = 9.6 EP added to HoJ.- Assumption, stated: only swings are counted as triggers and the extra attack's rage and on-next-swing interactions are not modelled, so this is a floor, not a sim result.
- Data:
Data/Vanilla/_core/UseEffects.luarebuilt at 1.15.9.69722 (HoJ is the only Vanilla item: 190 -> 191 proc rows, 334 -> 333 unscored);Data/Forever/_core/UseEffects.luaat 1.60.1.69913 (10 extra-attack items, 477 -> 467 unscored). - Superseded by "Hand of Justice priced on TBC, Wrath and Mists" at the top of this version: the
missing half on TBC and later was the RATE, and TBC and Mists now declare a
whiteDPS. Vanilla's row carriesch=2; Wrath's 44096 row carriesppm=1.08. - Wrath now declares
whiteDPS = 223inScoring/Wrath.lua's fallbackMODEL, so the heirloom Battleworn Thrash Blade (44096) prices its extra attack instead of scoring 0. 223 is the weapon DPS of Betrayer of Humanity (40384, Naxxramas-25 two-hander): (606 + 910) / 2 / 3.4, read from WoWSims WotLKassets/database/db.json. It leaves out attack power, so it is a floor. - Tools:
tools/tarball-read.py(deskitemdb-tarball) gained agrep:<path-end>:<regex>mode, becausedb.jsonis too large to print whole. The tool's command did not change. A bad regex or a spec with no:now prints one error line instead of a traceback (parse_grep, whichmainsends everygrep:argument through), pinned byTests/tarball_read_check.py(new, declared in.writ-suites.json). That check coversparse_greponly, so amainthat compiled the pattern inline again would stay green. Tests/libitemdb_spec.lua: seven specs (the warrior value, the hunter using the main-hand and not the ranged weight, the paperdoll override, a caster at 0, the itemized row's factors, the main-hand swing rate, a set stage). Red-checked by disabling the bucket: exactly the six value specs failed.- Peer Review 366810d1. E1: the share divided by
hitsPerSec, the same rate that multiplies a swing-triggered row, so the two cancelled and a dual-wielder (more hits than main-hand swings) priced HoJ low. It now divides byopts.attack.mainSwingsPerSec, elsemodel.mainSwingsPerSec, elsehitsPerSec(default scores unchanged). F2: the model / rate resolution was written at four sites and theweaponDPSoverride at two; new localprocContextserves all four (bothGetItemScore/GetItemScoreBreakdownitem and set paths,GetSetBonusEP).
[v1.6.0] - 2026-10-17
The first four sections below ship (a second Vanilla BiS provider; threat and armour
reduction priced in EP -- both MINOR 45; on-use rating trinkets priced from TBC on; IsBiS
returning a fresh list); the rest are under tools/ or Tests/, which the packager does not
ship, plus .pkgmeta (packaging only) and README.md.
Vanilla: a second BiS provider, "bisclassic", through phase 6 (MINOR 44 -> 45)
Data/Vanilla/_core/BiSClassic.luaregisterslib:LoadBiS("bisclassic", ...): 623 sets for 9 classes, phases 0-6, including the fire / nature / shadow / frost / arcane resistance lists. Thewowsimsprovider stops early (hunter at phase 1, mage and shadow priest at 1, rogue at 2, warlock at 0 + 2, warrior tank at 1; WoWSims master checked 2026-10-16), so nothing in the library answered "what is BiS in AQ40 / Naxx" for most classes (Dibs thread 000389037f53, the Badge of the Swarmguard report).wowsimsstays the default:ItemDB.tocloads it first.- Source: BestInSlotClassic (GPL-2.0, github.com/Warkdev/BestInSlotClassic) at commit
302ab4ecca0c, cached by the new
tools/fetch-bisclassic.py(deskitemdb-fetch, sourcebisclassic) and read by the newtools/build-bis-classic.py. Facts extracted, files not redistributed -- the AtlasLootClassic terms. Its model is a per-class/spec priority ranking plus each item's phase; the builder mirrors itsSearchBis(an item counts from its phase on, lowest priority wins). Choices the source leaves to its UI, made here: phase 0 is phase 1 without raid or world-boss items; a fractional phase (2.5) counts from the next whole phase; PvP-rank gear is left out; random-suffix rankings collapse to the bare item id. - Weapons and races (Peer Review 5ba689c6 B1-B3; the first cut got all three wrong and built
warrior fury phase 1-2 from the Orc main hand 17068 and the Human off hand 18832, a pairing no
race's list gives). Now: ONE SET PER RACE GROUP (the races whose rankings are identical), the
largest group on the plain token and the others suffixed with their race names
(
default_human,default_orc, hunterdefault_dwarf_nightelf); one-hander and two-hander priorities are never compared -- the source numbers them as separate lists -- so a spec itsWEAPON_ICONSoffers both setups gets the one-hand set on its token and the two-hander on<token>_2h(dropped when it ranks no two-hander); rankings are keyed by (item, OffHand), so an item the source ranks as both a main hand and an off hand (19363, 19104, 19170) keeps both.Tests/bis_classic_check.pygains fixtures for each, red-checked by restoring the merge and the item-only key (both went red, then green). - KNOWN, reported to Dibs: Badge of the Swarmguard (21670) is BiS here for Enhancement Shaman
(phases 5-6), not for hunters -- BestInSlotClassic ranks it 12th-13th among hunter trinkets,
behind Drake Fang Talisman and Jom Gabbar.
IsBiS(21670)now answers for the shaman. - CONSUMER-VISIBLE through an unchanged API:
IsBiS(itemID)with nosourcereads every loaded provider, so on Vanilla it now also returnsbisclassichits -- including the resistance lists. A consumer that shows "BiS for ..." from it without a source will show more. Pass"wowsims"to keep the old answer. - Items restricted to one faction (the source's
Factionfield) are not filtered: a set is faction-agnostic, likewowsims'. split-seasonal.pyskipsBiSClassiclikeBiS(it is keyed by class, not item).Tests/bis_classic_check.pypins the selection rule on a synthetic ranking.- Peer Review c9097ad3 B6-B8. New
lib:GetBiSSetSpecs(classID [, source]): the sorted spec tokens a FIXED SET source holds (default: the default set source), so a consumer findsdefault_humanortank_2hwithout rebuilding the naming rule.GetBiSSpecswas the only lister and reads the EP weight scales, which carry none of those tokens; its README row now says so. Same MINOR 45, because 45 is not released yet. READMEIsBiSrow: one hit per spec token per phase, so a warrior item in all three race groups' lists gives three a phase.build-bis-classic.pyrace_rankingsevaluated only the races some row named, so a playable race no row named got no set at all. NewPLAYABLE_RACES(the original-Classic race/class matrix): with an unrestricted row present every playable race is evaluated and an unnamed one gets the unrestricted rows, as the source'sSearchBisgives it; without one, a missing race or a named race the class cannot be is refused. The real cache builds the same 623 sets. Four new fixtures, red-checked by disabling the unnamed-race path (the check went red).
Vanilla and TBC: threat reduction and target-armour reduction priced in EP (MINOR 45)
- Operator, 2026-10-16: "threat reduction is HUGE for DPS classes". Fetish of the Sand Reaver
(21647, -70% threat for 20 s every 3 min), Eye of Diminution (23001, -35% for 20 s every 2 min),
Badge of the Swarmguard (21670) and, on TBC, Icon of Unyielding Courage (28121, ignore 600
armour for 20 s every 2 min) were in
LoadUnscoredProcs, scored 0, and Dibs' list hid them. tools/build-use-effects.py: the on-use path now emitsTHREAT_REDUCTION_PCT/SPELL_THREAT_REDUCTION_PCT(aura MOD_THREAT, sign flipped so a reduction is positive) andTARGET_ARMOR(aura 123 MOD_TARGET_RESISTANCE, negative, armour bit). Newuse_window: an on-use spell whose aura is a proc trigger (the Badge) emits the AVERAGE stack count over its window -- Poisson arrivals at the proc's CMaNGOSspell_proc_eventppm (26480: 15 per minute), capped by the triggered aura'sCumulativeAura-- 661.1 armour, not the 1,200 cap. New pure helpersuse_keys/average_stacks, pinned by the newTests/use_window_check.py(18 assertions, red-checked by restoring the old sign test).Scoring/Vanilla.lua/Scoring/TBC.lua: derived rules for the three keys through the spec's crit weight (1% crit taken as 1% damage, 0.5% for spell crit). Armour:(A + K) / 100points per 1%, VanillaA = 3731,K = 5500(WoWSims Classic level-63 boss and armour constant); TBCA = 7685(Magtheridon),K = 10557.5, through crit rating. Threat 1:1 with damage is an UPPER BOUND, ItemDB's own and stated as such; no community weight exists for it.LibItemDB-1.0.luaeffectWeight: a derived rule may carryexclude = { [classID] = { [spec] = true } }, read fromlib.bisWeightsSpec. Threat reduction excludes every tank and healer spec. A scale not registered throughLoadBiSWeightshas no spec and is not excluded.- Data:
Data/Vanilla/_core/UseEffects.luaandData/TBC/_core/UseEffects.luarebuilt at the builds their siblings carry; the diff is exactly those items leaving the unscored list. The post-buildsplit-seasonal.pyadded its partition marker line toData/Vanilla/_core/ItemPlaces.luaandTokenRewards.lua, which had not been through it before; no row changed. Tests/libitemdb_spec.lua: seven specs on synthetic rows (armour and threat pricing, the tank / healer exclusion, an unregistered scale), red-checked by disabling the exclusion.- Peer Review 917f21ea follow-ups. The exclusion list is ONE table,
lib._noThreatSpecsin the core, which bothScoring/Vanilla.luaandScoring/TBC.luaread (each carried its own copy). New specs: both flavours' threat rules hold that table; every spec it names is a spec the shipped Vanilla, TBC and Forever scales load (red-checked withferal_tnak); and no two specs in any flavour's shipped scale share one table, sincelib.bisWeightsSpecis keyed by table and a shared one would carry the last spec's exclusion. The TBC[2].defaultand[5].defaultscales are byte-identical toholyanddiscipline, so excluding them is correct.Data/Forever/_core/UseEffects.luarebuilt at 1.60.1.69913 (Forever loadsScoring/Vanilla.lua): 15 -> 17 on-use rows, 480 -> 478 unscored. - Peer Review a7919e37. The Badge of the Swarmguard (21670) still scored 0 on Forever:
use_windowread its ppm from CMaNGOS only, and Forever has no CMaNGOS DB. It now falls back to the version's OWN wowsims ppm (by the on-use spell, then by the item; a per-hit chance is not a ppm and is skipped), never Vanilla's CMaNGOS rate, whichbuild-sets.pyremoved as a defect. Forever rebuilt at 1.60.1.69913: 17 -> 18 on-use rows, 478 -> 477 unscored; the Badge prices at 481.3 armour (Vanilla, at CMaNGOS's 15 ppm: 661.1).Tests/use_window_check.pynow also reads Forever's file (26 assertions). New spec, the reverse of the token check: every tank or healer scale in the Vanilla, TBC and Forever weights (Stamina weighted 0.5 or more, or healing 0.5 or more and above spell damage) is inlib._noThreatSpecs. Red-checked by dropping shamantankfrom the list, and the shared-table spec by aliasing one Vanilla rogue scale: exactly those two failed. The_noThreatSpecscomment now names Forever.
TBC to Mists: on-use rating trinkets priced; the Badge scored on Wrath and Mists
build-use-effects.py'suse_keyshad no branch for aura 189 (MOD_RATING), so every on-use buff that grants a combat rating (haste, crit, hit, dodge, armour penetration, mastery ...) fell through to the unscored list on every flavour from TBC on. It now decodes the mask through the shareditemdb_common.rating_keys(misc, version), the tablebuild-effects.pyandbuild-aura-buffs.pyalready use, so an on-use trinket and a food carrying the same bit name the same key. Rebuilt, each at the build its siblings carry: Wrath 115 -> 166 on-use rows (940 -> 889 unscored), Cata 189 -> 273 (1421 -> 1338), Mists 264 -> 399 (1954 -> 1820); TBC now ships 83 on-use rows and 601 unscored. Vanilla and Forever are unchanged (no aura 189 in a 1.x client).- The Badge of the Swarmguard (21670) on Wrath: CMaNGOS wotlk has its ppm (15), but there its stacking aura is +28 armour penetration rating, not -200 target armour, so the missing rating branch was the whole cause. It prices at 92.6 armour penetration rating.
- On Mists: no CMaNGOS DB, and wowsims/mop does not model the Badge.
use_windownow also reads per-spell ppm from Mists' own SkyFire 5.4.8 world DB (mistsskyfire.sqlite, the samespell_proc_eventcolumns), Mists only, so Cata never takes Mists' number. 26480 is ppm 3 there; the stack is +28 crit rating on that client, so the Badge prices at 21 crit rating. Meteorite Crystal (46051, SkyFire ppm 5) also gained a use window: 100 spirit. - On Cata the Badge stays unscored: no world DB, and the cached wowsims/cata source has no Badge model (its only TBC trinket file is commented out).
- Peer Review cc49825a U1/U2: the build log now names the tier each use window took its ppm from
(Forever: wowsims/forever spell 26480, ppm 10, 2.407 stacks), and
use_window's docstring notes that the by-item wowsims tier keeps an item's first proc.Tests/use_window_check.pypins Forever'sm=481.3, the Wrath and Mists Badge rows, and fiveuse_keysaura-189 cases (33 assertions); red-checked by disabling the branch, exactly the three positive decodes failed.
IsBiS returns a fresh list (Peer Review cc49825a I1)
lib:IsBiS(itemID)with no source returned the reverse index's own list, so a consumer that sorted or trimmed the result edited every later answer for that item until the index rebuilt. Both paths now build a new list (the hit records inside are still shared). New spec mutates one result and asserts the next call is unchanged; it failed (3 expected, 4 got) before the fix.
Tools only -- the armor-curve verify says what it skipped (Peer Review 3748bed0)
- My TBC self-audit said shields went unchecked because wago serves no
ItemArmorShieldat 2.5.6.68941. Wrong: no TBC shield carries walk armour (507 of them, the TBC walk's KNOWN COST), soverify_armorskipped every one at itswant <= 0test before the table mattered, and the run printed only exact / low / OVER -- a green line that read as if shields were covered. The verify now prints a "not checked" line: shields and cloth/leather/mail/plate with no walk armour (the latter named, up to 12). TBC at 2.5.6.68941: 507 shields and 6 body pieces skipped (4728, 5091, 19837, 33073-33075 -- ilvl-0 placeholders, a shirt-slot and a ranged-slot piece), 11,362 exact, 0 OVER. The comment that claimed the unchecked count stops a TBC run is corrected: it guards only a version whose walk carries shield armour (Vanilla, Forever, Mists, Retail).verify_armornow returns a third value, the skipped counts. - A hidden item (test / placeholder, never served) that comes out OVER is excused, named and not
failed; hidden items that match still count as exact. Forever's OVER row 16213 is "Test Quality
Modifier Chest", whose armour is whatever a developer typed. (The first cut skipped every hidden
row, which cut TBC's exact matches from 11,362 to 9,769 -- caught in the self-audit.)
hidden_idsmoved frombuild-core-wago.pyintoitemdb_commonso both builders read Hidden.lua one way. - Forever's other OVER row, 273646 "Half-Eaten Boots" (walk 43, curve 143), is now a listed
ARMOR_EXCEPTIONSentry, printed by name on every run. The row is still compared: only an OVER result is excused, and an entry whose row is no longer OVER prints as STALE so it cannot go on excusing nothing. Its cause is NOT VERIFIED: wago's ForeverItemSparseat 1.60.1.69913 has no row for it (itsItemrow is there). A "-100 Armor" bonus would fit the gap exactly. Forever's verify now exits 0 (7,537 exact, 592 low, 0 OVER); Vanilla passes too (6,715 exact, 12 low, 0 OVER). Tests/armor_curve_check.py13 -> 18 checks. The skipped-count check was written first and failed on the old two-value return.
Docs -- the changelog back under GitHub's release-body limit
- CHANGELOG.md was 127,330 characters, over GitHub's 125,000-character release-body limit, so the next release would have been rejected with HTTP 422. v1.1.3, v1.1.2 and v1.1.1 moved unchanged to CHANGELOG_ARCHIVE.md; checked section by section (32 before, 32 after, no word changed).
- markdownlint's line-length rule (MD013) stays off. Turning it on at 80 columns (Peer Review b4eb143b, M1) was tried and reverted on the operator's ruling, 2026-10-17: "there is only one line length rule, you need to move stuff from the changelog into the archive". The one length this repo keeps is the changelog's size, kept by archiving the oldest whole versions. The 80-column reflow tool written for M1 was removed with it.
- The
markdownlintsuite was shown to fail (with MD013 on it exited 1, so the npm shim passes the failure through), and it now lints CHANGELOG_ARCHIVE.md too (Peer Review b4eb143b, M2). - CLAUDE.md: "merge- precedence" corrected to "merge-precedence", and the README rules' closing paragraph ("Write it for a dev...") moved out of the last nested bullet it had been read into.
Tools only -- derive-core-maps.py re-run on Vanilla, TBC and Mists (Peer Review d89d615b)
- First run of
read_walk_coreonitemdb_common.core_rowssince the v1.4.1 refactor: every InventoryType maps to one token on all three versions, matchingitemdb_common.INVTYPE. Every stat id's top key matchesbuild-core-wago.STAT_KEY, orEXTRA_ARMOR/RESIST_STATfor ids 50-56, except Mists ids 11 and 40 (2 items each, no mapping; not investigated). - The structural check now NAMES every item where wago and the walk disagree (the full list, not a truncated one), instead of only counting them. TBC's 2 are the WoW Tokens (122270, 122284), which the client reports as quality 8 with no subclass. Mists has 98 quality, 18 item-level and 7 subclass differences, where the shipped walk value is the client's own answer; nothing shipped changed.
- The comment claiming the true stat key "wins by orders of magnitude" is replaced with what was measured: about 3-4 to 1 for the primary stats, with ratings among the runner-ups on TBC/Mists.
- A stat id with five or fewer hits now lists its items. That named Mists ids 11 and 40: Test
Sword Chest 13712, Test Sword Skill Trinket 33003 and Juno's Test Gem 34967, all in
Data/Mists/_core/Hidden.lua.build-core-wago.pydocuments them as deliberately unmapped. build-core-wago.pygainsverify_structure(): before it writes Wrath/Cata it reports where wago's quality, subclass, equip slot and item level disagree with the TBC and Mists walks, whichverify()(stats only) never compared. Its docstring said the gate compared "every emitted key on every item", which read as covering structure; it now says the gate is stats only. Report-only, because no Wrath/Cata client is on hand to say which side is right;Tests/core_wago_check.pyholds the counts exactly (TBC: the two WoW Tokens; Mists: 98 / 7 / 18), so a new drift goes red. Red-checked by reading item level from the wrong field: both counts failed.- Forever also run: its ItemSparse carries no stat amounts, so nothing matches by value; its core is walk-built, so nothing shipped depends on that.
- Peer Review c53c25e2 nits:
verify_structure's docstring names the equip slot it compares, and the headerbuild-core-wago.pywrites into Wrath/Cata files now says stats are gated and structure only reported. The shipped Data headers pick that up at their next rebuild.
Packaging -- .pkgmeta cleaned, and its checker declared
.pkgmetacarried comment lines, which the no-comments rule forbids; removed. One was# curseforge-project-id: 1558208, which the packager never read:release.shtakes the id from-por the TOC's## X-Curse-Project-ID: 1558208(inItemDB.toc).- Dropped the
"*.sh"and"*.bat"ignore entries; no tracked file matches either. - The harness's
.pkgmetachecker is now the declaredpkgmetasuite in.writ-suites.json. Its WARN [7] (nocurseforge-project-id:) asked for a keyrelease.shdoes not read; reported to WoWAPITesting (inbox d446ea6d), fixed in harness f6e5fde, which reads the TOC id instead.
Docs -- GetTokenRewards named a GetSlot the library does not have (Dibs, inbox 65ade047)
- The comment above
lib:GetTokenRewardsand itsREADME.mdrow told consumers to read each reward's slot withGetSlot. No such method exists, so following that raised a nil-method error. Both now nameGetInfo's 5th return (equipLoc). Comment and docs only.
Tests -- harness pin 407bd26 -> f6e5fde
- Harness f5389cb runs a block's examples after its whole body, as busted does. That left
Tests/pricewindow_spec.lua's file-levelrequire("env.frames")+libs.load("LibAceGUIWidgets-1.0")in place before its two "without the widget library" examples, and both failed. The load now happens once, in the window block's firstbefore_each. Suite 926/926.
[v1.5.0] - 2026-10-16
Added -- a Shaman tank weight scale (Dibs' contract 46c74258)
GetBiSWeights(7, "tank")now resolves on Vanilla, TBC and Forever, in the defaulthawsjonsource. HawsJon publishes no Shaman tank scale and no published Classic one was found, sotools/build-bis-weights.py's newDERIVED_SPECScopies HawsJon's own Protection Paladin scale -- the one published model of a mana-using shield tank whose threat comes from spell damage. Each generatedBiSWeights.luasays in its header that[7].tankis a copy, not HawsJon's, and the builder refuses to copy once HawsJon publishes the spec itself.- The copy drops
DPS_OFFHAND(DERIVED_DROP). A paladin can never dual-wield, so that weight never applied to its own scale; a TBC shaman can (Dual Wield, 30798), and keeping it would let an off-hand weapon compete with a shield on a tank scale. Dropped on all three versions so they share one rule (Peer Review 4c1a17ee P3). PARRY_PCTis kept, verified from the client data rather than the talent's name: Enhancement talent 16268 ("Parry" on Era, "Spirit Weapons" on TBC) learns spell 18848, whoseSpellEffectis the same effect (type 22) as the baseline Parry spell 3127, in both the 1.15.9.70003 and 2.5.6.68941 tables. Shield Specialization backs the block weights and Anticipation the dodge weight. Forever's tree was not checked. (The first cut of this entry and the builder comment said TBC had noTalents.luaand that Anticipation gives defense; both were wrong -- Peer Review 4c1a17ee P1/P2.)itemdb_common.headerwrites a blank header line as--with no trailing space after it. Every builder's generated headers lose that whitespace the next time they are rebuilt.- No scorer change: the Vanilla/TBC scoring rules carry no spec-keyed tables, and the
per-class
ScoreModeldefault applies. No MINOR bump -- the API is unchanged and Dibs detects the scale byGetBiSWeightsreturning non-nil. - New
Tests/derived_weights_check.py(declared in.writ-suites.json, red-checked by editing one weight in the shipped row, then by puttingDPS_OFFHANDback) pins that each derived scale equals its source minus the dropped keys, that the header names it as a copy and each drop, and that[7].defaultis stillelemental.
Added -- what a tier token is turned in for (MINOR 44, Dibs' contract 1c5f8e8d2faf)
DB:GetTokenRewards(itemID)returns the gear a boss-dropped token is exchanged for, every class's together, as a fresh table sorted by item id, ornilfor an item that is not a token (never an empty table). Dibs prices a token by a substitute item level and slot, and asked for a default derived from the rewards.lib:LoadTokenRewardsreplaces per token. MINOR 43 -> 44.- New
tools/build-tokens.py->Data/<V>/_core/TokenRewards.luafor Vanilla (87 tokens), TBC (159), Wrath (226) and Mists (115), listed in those four TOCs. The client has no such table; the exchange is server-side, so it reads the same world DBbuild-droprates.pydoes:quest_templateturn-ins (every Vanilla token) andnpc_vendor/npc_vendor_templateExtendedCostagainst the client'sItemExtendedCostDB2 (TBC onward). A token is an item required by count 1, not itself equippable, dropped by an encounter inSources.luaorDropRates.lua, and rewarding equippable gear in that version's_core. - Three defects found while building it, each fixed and pinned by the new
Tests/tokens_check.py(declared in.writ-suites.json, red-checked):- Head of Onyxia and Head of Nefarian were missing: quest 7490 takes the head and rewards
nothing; its
NextQuestInChain7491 needs no item and rewards the rings. A reward-less turn-in is now followed down its talk-only chain (depth 3). - Chestguard of the Fallen Defender listed six Gladiator pieces beside its seven T4 chests.
tbc-db prices Karynna's Season 1 stock (template 555) at the same
ExtendedCost1235 the T4 vendor uses, and the 2.5.6 client reads 1235 as one T4 chest token. A vendor now counts only when most of its cost rows resolve to a token (template 554 and npc 26089 fail that, at exactly half), and a reward carrying resilience is never a token reward. Every T4, T5, T6 and Sunwell row now matches AtlasLoot's own token table (AtlasLootClassicData/Token.lua, BCC); 29753 and 30243 are pinned against it. - SkyFire's
quest_templatehas no required-item columns, so Mists refused to build; the quest half now degrades to nothing there and the vendor half carries it.
- Head of Onyxia and Head of Nefarian were missing: quest 7490 takes the head and rewards
nothing; its
- Not covered, stated rather than hidden: Forever, Cata and Retail ship no file: the pipeline has no world DB for any of the three. Mists carries the Wrath-era tokens only; why Pandaria's own tier tokens are absent was not investigated. Wrath's Trial of the Crusader trophies were not checked. Nothing ran in a live client.
Fixed -- the armor-curve verify hid its own failures (tools only, nothing ships)
tools/build-core-wowsims.pyverify_armorprinted low and OVER rows from one list capped at 12. Low rows (items with bonus armor, expected, often hundreds) filled the cap first, so the Forever run of 2026-10-12 reportedOVER 2and named neither item. OVER rows are the failures, so they now print in full under their own heading; low rows keep the first-12 sample.- That run's two OVER items are 16213 (walk 531, curve 541) and 273646 (walk 43, curve 143).
16213 is "Test Quality Modifier Chest", flagged hidden, and its Forever
ItemSparserow (1.60.1.69913) carriesQualityModifier=-10, whichbase_armordoes not read; whether that field accounts for the 10-armor gap is not verified. 273646 ("Half-Eaten Boots", Forever-only) has no row in the cached ForeverItemSparseat that build, so the walk came from a newer client and the item cannot be inspected at the pin. Players are unaffected: Forever's core is walk-built, so its shipped armor is the client's own figure and this formula does not reach it. Rerun 2026-10-12 with the fix: both rows print underOVER:; Vanilla unchanged (6,715 exact, 12 low, 0 over). - No shipped flavour carries curve-built armor today: every
Data/*/_core/Armor.luaheader (and Vanilla's_seasonal/Armor.lua) namesbuild-core.py(a walk) orbuild-core-wago.py, andbuild-core-wowsims.pyrefuses to overwrite either. So neither OVER cause reaches a player. armor_curvestreatsItemArmorShieldas optional. wago answers HTTP 400 for it at TBC 2.5.6.68941 while serving the other three curve tables, and the TBC verify died there before checking a single piece. Shields are now "not computable" (base_armorreturnsNone) and--verifycounts them asunchecked, never as low (low means "carries bonus armor"). A 504 or any other error still raises. The TBC verify itself has not run yet: wago answered 504 at both 2.5.6.68941 and 2.5.5.67511 the same day.- Peer Review (ad843502): unchecked rows first sat outside the total, so
--verifyexited 0 with every shield skipped, and a build at such a pin would have shipped every shield with no base armor. Now unchecked rows count toward the total and never toward a pass, so--verifyexits 1 while any remain; and the newrequire_shield_curvemakes the build refuse when the shield table is absent and any shield would ship. A wrong 400 then costs a rerun, never bad data. - New
Tests/armor_curve_check.py(declared in.writ-suites.json, 13 cases) pins that path, the 504 re-raise, the build refusal, the failing partial verify and the robe-reads-chest row. Red-checked three ways: theNoneguard replaced with an empty table (got 0), the old return restored (got(1, 1)), and the refusal short-circuited (gotFalse). - New repo desk tools:
itemdb-verify-pinned(a builder's--verifyat an exact--wago-build, so the check reads the build the shipped files carry -- wago.tools answered 504 for the installed 1.15.9.70003 while 1.15.9.69722 is cached) anditemdb-derive-maps(tools/derive-core-maps.py --version V --wago-build B, read-only;itemdb-buildcannot pass its version flag).
[v1.4.1] - 2026-10-12
Fixed -- where-to-get places filed in the wrong zone (Dibs' DIBSREQ-IDB, Searing Whelp)
Dibs' report (inbox 2c393671f8ee): Dibs showed Small Flame Sac dropping from Searing Whelps in
Thousand Needles on Classic Era. The whelps live in the Wyrmbog, Dustwallow Marsh.
- Root cause:
tools/build-item-places.pyfiles each spawn under the zone whoseUiMapAssignmentrectangle holds it. Those rectangles are map-image bounding boxes and overlap far past the real borders: every Searing Whelp (4324) spawn is inside the Barrens, Thousand Needles AND Dustwallow rectangles, and the tie-break ("the smallest rectangle") chose Thousand Needles. No rule over the rectangles alone gets it right (smallest, deepest-inside and nested were all tried), and the CMaNGOScreature_zone/gameobject_zonetables are empty. - New
tools/fetch-questie.py(deskitemdb-fetch, sourcequestie) caches QuestieDB's (github.com/Questie/QuestieDB) NPC and object spawn tables for Classic, TBC, Wotlk, Cata, MoP and Forever at a pinned commit (cd4ac424,tools/wago_cache/questie.source.json), each file verified against its git blob id, and reads them: per entity,{AreaTable zone: [(x, y)]}. It sharesfetch-att.py's GitHub helpers (resolve/fetch_onenow take the repo's API, cache and raw roots, defaulting to AllTheThings'). Placer.placestakes that as a hint: a spawn inside several rectangles goes to the zone QuestieDB files the entity under (nearest recorded point when it lists several candidates), then the zone the entity's unambiguous spawns use, then the smallest rectangle. The AreaTable id becomes a UiMap through the client's ownUiMapAssignment.AreaID(zone_areas, a SET of UiMaps per area, so an area on two zone maps hints both). The coordinates shipped are still the CMaNGOS spawn's; QuestieDB only picks the zone, and none of its data is shipped. QuestieDB carries no licence file (GitHub's licence API answers 404, recorded inquestie.source.json); it is a build-time input only.- A hint whose nearest QuestieDB point is more than
HINT_MAX_YARDS(250) from the spawn gives way to the entity's clear spawns, but still beats the smallest rectangle. Measured on shipped spawns before choosing it: Vanilla, the same world as QuestieDB, median 0 yards, 99th percentile 49, max 4,644; Cata, whose places are SkyFire 5.4.8 spawns against QuestieDB's 4.x points, median 0, 90th 50, 99th 560. It moved four entities on Cata and Mists, none elsewhere: Coral Shark (5434) drops The Cape of Stranglethorn, Winterfall Runner (10916) drops Felwood, and the Firefin Snapper and Oily Blackmouth schools (180683 / 180682) drop Thousand Needles. Which side is right for those four is not verified. tools/fetch-questie.pyrefuses a line that opens a row ([<id>]) but is not one whole[id] = {...},line, instead of skipping it, and looks the licence up before its already-pinned early return so a cache pinned offline does not keep "not reached".- The builder reports which rule settled the overlapping spawns -- summed over the entities it
ships, not every entity it placed (a vendor weighed for the zone spread and not chosen used to
count) -- the distance spread above, any QuestieDB area that sits on more than one zone map
(none on any flavour; its points would be measured in a frame that may not be theirs), and how
many places changed zone against the file it replaces AND against git HEAD. Rebuilt every
flavour QuestieDB covers; places that changed zone against HEAD, the cap included: Vanilla 775
of 4,725, TBC 916 of 6,510, Wrath 1,213 of 8,559, Cata 1,253 of 9,601, Mists 1,351 of 10,216.
Vanilla's shipped overlapping spawns: 36,450 settled by QuestieDB, 344 by the entity's
clear spawns, 96 by clear spawns over a far hint, 14 by a far hint, 33 by the smallest
rectangle. Checked in the built file, not in a client: Searing Whelp to Dustwallow Marsh,
Skeletal Warrior (48) and Gavin Gnarltree (225) from Deadwind Pass to Duskwood, Gruff Swiftbite
(100) from Duskwood to Elwynn Forest, Mine Spider (43) to Elwynn Forest. Vendor choice shifts
slightly where a vendor's zone changed (vendors are spread over zones), so a few translated
names enter or leave
PlaceNames.lua. Retail is unchanged: QuestieDB stops at Mists. Vanilla is pinned to 1.15.9.69722, the build its siblings carry. Tests/item_places_check.py: the hint against both old rules, nearest-point choice, an area on two zone maps, the distance cap both ways, the shipped-only report, a split row refused, the move report, and the real case on real data -- Searing Whelp places in 1445 only, and without the hint it still comes out 1441, so the check can fail. A missing cache fails it rather than skipping.
Changed -- Peer Review f4bc13b5 follow-ups
Tests/itemdb_common_check.py's every-[id]=assertion now red-checks itself on every run: a scratch tree with one rowcore_rowscannot read must come out unequal.Data/Vanilla/_core/EquipStats.lua(hand-edited for the earlier red-check) is rebuilt by the builder at 1.15.9.69722 and re-split, rather than restored by hand.- New desk tool
itemdb-verify(repo scope) runs a builder's read-only--verify, whichitemdb-buildcannot pass.
Fixed -- gear school resistances on Vanilla, TBC and Wrath (MINOR 43, Dibs' DIBSREQ-IDB)
Dibs' contract (inbox f93b32b37ec4): a frost-resistance sort in Dibs' Planner found almost
nothing on Classic Era, because GetStats returned no school resistance for most gear. Glacial
Cloak (22658) had stamina and armor but not its 24 frost; Polar Tunic (22661) was missing 40
frost and Arcanist Robes (16798) 10 fire.
- Root cause: the Classic clients'
GetItemStatsnever reportsItemSparse.Resistances_1..6(0 items in the Vanilla and TBC walks, asbuild-core-wago.pyalready recorded), so the walk-built cores carried only resistance that comes from an on-equip aura (16061: column 0, walk fire 9). The Wrath core reproduces the TBC walk and so lacked them too. tools/build-equip-stats.pynow readsResistances_1..6in its existingItemSparsepass and emitsRESISTANCE<n>_NAME(1 holy, 2 fire, 3 nature, 4 frost, 5 shadow, 6 arcane) intoEquipStats.lua, whichGetStatsmerges. An item whose core ALREADY carries a school (an on-equip aura the walk reported) and whoseItemSparsecolumn names the same school would need the two summed, whichGetStats' core-wins merge cannot do -- so the builder REFUSES and names the item (Peer Review13334356F1; the first cut only counted it on stderr). None exists on any version today.- Rebuilt: Vanilla 1,371 items (pinned to 1.15.9.69722, the build its siblings carry, then
split-seasonal.py), TBC 667, Wrath 723. Forever gains none: itsItemSparsecolumns are empty and its walk already reports resistances. Mists, Cata and Retail store resistances as stat slots, which their cores already read; not rebuilt. itemdb_common.core_rowsis now THE reader of the shipped class files (CORE_ROW+ thelib:LoadCore(test), andcore_idsis a view over it. It replaces four private copies of the row regex and file walk inbuild-equip-stats.py,build-core-wago.py,build-core-wowsims.pyandderive-core-maps.py(whose hand-keptNON_ITEMside-table list is gone). Found through it:core_resist_keysassigned per row, so an id with a base AND a seasonal row kept only the last file's schools (Peer Review13334356F1b); it now takes the union.Tests/data_baseline.jsonre-recorded. The 14 shrinks it accepted were all already in the released v1.4.0 tree, HEAD being that release:Data/Retail/_core/Effects.lua1,186 -> 1,182 (-4),Data/Vanilla/_seasonal/Quest.lua451 -> 450 (-1) and the twelveData/Vanilla/_seasonal/<locale>/Names.lua6,523 -> 6,522 (-1 each; the same item moved to the baseQuest.lua/Names.lua, both +1). Also adopted: RetailHidden.lua(new, 13,923) and the grownHidden.luaon Cata, Forever, Mists, TBC and Wrath.- MINOR 42 -> 43 so a consumer can gate on the data (
select(2, LibStub("LibItemDB-1.0")) >= 43); no API change.CLAUDE.md's MINOR rule now says so: it moves for an API change OR when a consumer needs to detect new data (Peer Review13334356F2). Tests/equip_resist_check.pyred-checked by removing Glacial Cloak's row from the Vanilla file: it failed on that item, and passed again once restored.
Older versions (v0.0.1 to v1.4.0) are in CHANGELOG_ARCHIVE.md.
This mod has no additional files

