• v4.3.2 34a40577b5

    v4.3.2
    All checks were successful
    CI / build-and-test (push) Successful in 1m50s
    Stable

    joeyr released this 2026-09-05 20:20:05 -05:00 | 24 commits to main since this release

    Fixed

    • A build the Item Import panel refused to show cost a card slot instead of
      moving down the list.
      Both item adapters truncated core to the top
      MAX_ITEM_OPTIONS and validated afterwards, so a row that could not be
      rendered left a hole rather than yielding to the next real build. The row
      that does this is not exotic: the producer publishes a one-item core for a
      game that completed a single item, coreItemIds is min(2) precisely to
      keep those out of an import, and they are common enough to land inside the
      top four — 236 of 555 champion-roles on the 16.17 file. Amumu jungle showed
      three cards with a fourth (Sunfire → Liandry’s) sitting unused directly
      below the cut; Briar jungle showed three with two valid builds stranded.
      Both adapters now walk the ranking and stop once four rows have passed
      validation. The floor is unchanged — one-item rows are still refused, they
      just no longer take a slot with them.
    Downloads
  • v4.3.1 48c6b10adb

    v4.3.1
    All checks were successful
    CI / build-and-test (push) Successful in 2m52s
    Stable

    joeyr released this 2026-09-03 20:30:13 -05:00 | 25 commits to main since this release

    Fixed

    • The T3 badge in champ select had a border you could not see. Every tier bucket draws its badge as colored text inside a matching 1px border, but T3 - the neutral middle bucket - set only the text color and left the border at the panel default --border (#2c3540), a shade off the badge's own --panel-2 background (#232b36). Next to a green T2 or an orange T4 the box read as missing rather than neutral. T3 now carries its own slate #7c8798 border: visibly a box, still uncolored, so it does not compete with the buckets whose color is the signal.
    Downloads
  • v4.3.0 48f973c62e

    v4.3.0 — Scouting report opens with a 15-request burst
    All checks were successful
    CI / build-and-test (push) Successful in 1m29s
    Stable

    joeyr released this 2026-09-01 19:54:21 -05:00 | 27 commits to main since this release

    Added

    • The scouting report opens with a 15-request burst. Every Riot call in the
      app draws on one shared token bucket at 0.65 req/s, so a run's opening rows
      trickled out at roughly one every 1.5 seconds: the tenth player's rank landed
      half a minute into a game that was already being played. TokenBucketRateLimiter
      now takes a burstCapacity, and ScoutService.run() primes it once at the top
      of a run — 15 tokens at once, enough to cover rows 1-2 (10 account-v1 + 10
      league-v4, minus whatever the identity cache already holds), which is the
      identity and rank the report is useless without. The deep tier still finishes
      at the steady cadence, which is the right place to be slow.

      The allowance is granted, never accrued: idle time does not build toward
      it. That distinction is load-bearing, because the benchmark harvester shares
      this bucket and asks for no burst — had the burst been part of the steady
      capacity, a harvest resuming after a quiet stretch would have fired fifteen
      unbudgeted requests at the shared key. Priming is idempotent for the same
      reason a scout run can be re-triggered mid-game: a second prime tops the
      balance back up to 15 rather than stacking a second burst.

      Budget note. 15 sits inside Riot's ~20-req/s short window, but it is 15
      requests the 2-minute budget did not previously spend. The same personal key
      is held by aufhocker, and the two limiters already sum over the key's 100/2min
      (see the note in wireRiotClient); this widens that overrun by 15 per scout
      run. It is bounded — once per run, not per player — and the client's existing
      429 retry covers the case where it lands on top of aufhocker's own
      game-detection burst. Lower burstCapacity there if 429s become routine.

    Downloads
  • v4.2.0 a941ee2d04

    v4.2.0
    All checks were successful
    CI / build-and-test (push) Successful in 1m27s
    Stable

    joeyr released this 2026-09-01 19:30:10 -05:00 | 28 commits to main since this release

    Changed

    • The counters, synergies and builds mirrors accept
      generatedFrom.previousPatch.
      lol-companion-data is about to publish those
      three files over a pooled two-patch window, and the new field names the second
      patch. It is optional and nullable here while the producer's copy is required
      and nullable — that asymmetry is the whole point of the release. These schemas
      are .strict(), so a producer that shipped the field first would have every
      install reject the file and freeze on cached data until the app updated. The
      mirror has to land first, and this is it.

      generatedFrom.patch is untouched. It still names the patch the file is for
      and is still what the app compares against the LCU before accepting a fetch —
      pooling widens what was measured, not what the file claims to describe.

    • stale on a benchmarks bucket now means "the current patch has measured
      nothing here yet."
      Benchmarks pool both patches upstream, so a bucket's n
      is the sum of the two and stale no longer marks an either/or fallback. The
      runtime already labels those buckets with generatedFrom.previousPatch, which
      stays correct under the new meaning, so nothing about what the overlay renders
      changes. The damage artifact's identically named flag does still mark a real
      fallback; the two are no longer the same signal, and the schema comments now
      say so.

      No behavioural change ships in this release. It exists so the schema change
      can land before the data does.

    Downloads
  • v4.1.0 d855960a77

    v4.1.0
    All checks were successful
    CI / build-and-test (push) Successful in 1m57s
    Stable

    joeyr released this 2026-09-01 18:40:23 -05:00 | 29 commits to main since this release

    Added

    • A second champion pool, for Classic Rift. The Settings panel's "Suggested
      picks" card now carries two per-lane pools: the existing one and a new
      Classic Rift Champion Pool picks block with the same five lanes. Champ
      select reads whichever matches the game it is in — the roster is taken from the
      gameflow map id (453 = classic), the same signal the board's mode badge uses —
      and a lane left empty falls back to meta exactly as before.
    • The two lists are deliberately not merged. Classic champions reuse their
      base champion's display name but not its kit (Jade Annie has 460 HP and 550
      attack range against live Annie's 560 and 625), so "the champions I play here"
      is a different answer, and a name identifies a champion only within a roster.
      Pool names are now resolved against the half of the champion directory the
      session belongs to, so a classic pool's "Garen" resolves to 60086 rather than
      quietly ranking id 86's matchups.

    Changed

    • The settings file gains myClassicPool, alongside myPool and with the same
      shape and opt-in semantics. An existing settings.json needs no edit: an
      absent value means "no classic pool", which behaves as it does today.
    • The champion search box in each pool block no longer lists duplicate names. It
      is fed by the id-keyed champion map, which carries both rosters, so every
      champion with a classic counterpart appeared twice.
    • Editing the classic pool does not enqueue a benchmark harvest. Match v5 has no
      Classic Rift games — queue 4310 results 403 permanently — so those would be
      requests that cannot land. Live-pool edits pre-harvest as before.
    Downloads
  • v4.0.3 13ba2debb6

    v4.0.3 — lower the shared-key Riot rate to 0.65 req/s
    All checks were successful
    CI / build-and-test (push) Successful in 1m51s
    Stable

    joeyr released this 2026-08-25 20:26:34 -05:00 | 30 commits to main since this release

    Changed

    • Riot client rate 0.75 → 0.65 req/s (~90 → ~78 requests per 2 min). The
      0.75 was sized as though this app were the only holder of the Riot key. It is
      not: aufhocker's live monitor polls the same personal key through its own
      limiter, which knows nothing of this one. What Riot enforces is the sum of the
      two, and at 0.75 that sum was ~165 against a 100-per-2-minutes cap — so the
      bot took sustained 429s while spending only ~4% of the budget itself
      (aufhocker 1.13.0 has the full account).
    • This narrows the overrun without closing it. aufhocker now holds a
      40-per-2-min ceiling, so the pair sums to ~118 — still above 100. 0.65 was
      chosen deliberately to protect harvest throughput, accepting that 429s remain
      possible when both run at once. aufhocker logs mine / key / others once
      the key-wide count passes 80% of the cap, which is where to look if they do.
    Downloads
  • v4.0.2 8ed5b79f68

    v4.0.2
    All checks were successful
    CI / build-and-test (push) Successful in 2m51s
    Stable

    joeyr released this 2026-08-10 22:01:45 -05:00 | 31 commits to main since this release

    Fixed

    • "Your Pool This Patch" was quoting an ally pairing from the wrong lane. The
      champion-synergies artifact splits every pairing by the ally's role — Hwei mid
      and Hwei bottom are separate rows — but the row rendered beside a pool entry
      reads "50.0% with Hwei · 62 games" and says nothing about which lane Hwei
      played. The consumer matched on champion id alone, so whichever single lane
      happened to clear the 50-game floor was shown beside a teammate hovering that
      champion in a different lane. In a real champ select, Briar's Hwei row was a
      62-game mid-Hwei sample displayed next to a bot-lane Hwei; the pairing that
      actually applied was 27 games at 29.6%.

      The indexer now sums each ally's rows across the roles it was played at and
      applies the floor to the total, so a row means what its label says: games with
      that champion anywhere on the team. Summing is exact rather than an
      approximation — the artifact carries wins per row, so the combined rate is
      Σwins / Σgames, not an average of rates.

      Splitting by ally lane was also the wrong statistical call for this section.
      The same file carries no measurable pairing effect at any sample floor (see
      src/shared/champion-synergies/schema.ts), so subdividing a pairing by lane
      subdivides noise, and the thin slices read louder: Nocturne beside a bottom
      Hwei is 66.7% over 18 games, while Nocturne beside Hwei anywhere is 59.1% over
      71. Against the patch-16.15 file (26,698 matches, 119,760 pair rows) the change
      lifts qualifying pairings from 3,980 to 5,331 — about a third more evidence at
      the same floor, not a lowered bar.

    Changed

    • SynergyAllyRow no longer carries allyRole, and indexChampionSynergies
      emits one row per ally champion rather than per (champion, role). This also
      retires a latent duplicate-React-key case in PoolStrengthSection, which keys
      ally lines on the champion id.
    • MAX_ALLY_EVIDENCE_PER_ENTRY stays at 2, and is now what usually limits the
      rows on screen rather than the game floor: a full team of hovering allies
      routinely puts three or four pairings over the floor, and the two best-sampled
      win. A qualifying pairing can be computed and not shown — intended, and now
      documented at the constant.
    Downloads
  • v4.0.1 883cc34872

    v4.0.1
    All checks were successful
    CI / build-and-test (push) Successful in 1m32s
    Stable

    joeyr released this 2026-08-09 00:01:41 -05:00 | 33 commits to main since this release

    Changed

    • The test suite runs in about a third of the wall clock, with no coverage
      given up.
      Two independent causes, both pure waste:

      • Renderer interaction tests now share a setupUser() helper that passes
        delay: null to userEvent. The default delay: 0 awaits a real macrotask
        between every sub-event of an interaction (pointerover → pointerdown →
        mousedown → focus → pointerup → mouseup → click), so an interaction test's
        cost was set by timer scheduling rather than by the component. The same
        events fire in the same order with the same assertions available between
        them; only the waiting is gone. In isolation settings-panel.test.tsx went
        from 4191 ms to 937 ms.

      • Only 21 of the 86 unit test files touch the DOM, but all of them ran under
        jsdom. test/unit/renderer/** is now its own renderer project and the
        other 65 run in node. That cost was invisible in the per-file durations
        Vitest prints — about 1 ms there — because it lands in the run summary's
        environment total, which was 510 s on CI.

      Locally the full suite went from ~45 s to ~17 s, and environment from 264 s
      to 57 s. npm run test:unit runs both halves, so the split is invisible to
      the scripts and to CI. All 1054 tests still pass.

    Fixed

    • Two Vitest projects were handed the same define object reference. Vitest
      mutates it in place while resolving a project, so the second clobbered the
      first and __APP_VERSION__ survived as a bare identifier — every test that
      renders App died on ReferenceError. Each project now spreads its own copy.
      No user-facing effect; recorded because the failure is remote from its cause.
    Downloads
  • v4.0.0 3cf913eed7

    v4.0.0
    All checks were successful
    CI / build-and-test (push) Successful in 3m8s
    Stable

    joeyr released this 2026-08-08 22:54:17 -05:00 | 34 commits to main since this release

    Changed

    • The op.gg scrapers are gone, and with them every fallback in the data
      layer.
      Counters, tier badges and rune pages each used to consult the
      published artifact first and then scrape op.gg live for anything it could not
      answer. That second path is deleted. Every provider now reads one published
      artifact, and a question it cannot answer is reported as unanswered rather
      than filled in from somewhere else.

      The reasoning is that the two paths were never the same answer. The crawler
      fetches those op.gg pages overnight, at its own pace, under fixed filters; a
      mid-draft scrape fetched a different page at a different moment. A champ
      select could be served by both, and nothing on screen said which — so a
      defensive-ban suggestion, a tier badge, or the rune page the Import button
      wrote could depend on which source happened to answer first.

      What you will notice: on patch-drop morning the board shows no counters and
      no tier badges
      until the crawler publishes for the new patch. Those
      artifacts refuse an off-patch copy on purpose — a tier list is a claim about
      this patch's meta — and the scrape that had already rolled over is no longer
      there to cover the gap. A blank badge is visibly blank; last patch's meta
      rendered as this patch's is not.

      Caches are unaffected and are not fallbacks under this rule: every provider
      still serves its own last good copy when a fetch fails, and damage splits and
      overlay benchmarks still serve an off-patch copy, because a champion's kit
      survives a patch that did not touch it.

    Removed

    • Settings.statsSource and Settings.rateLimitPerSecond. One selected between
      op.gg adapters and the other paced their page fetches; with no scraper there
      is nothing to select and nothing to pace. Both keys are ignored if present in
      an existing settings.json and dropped the next time it is written — no
      action needed. Riot API traffic (the scouting report and the benchmark
      harvest) keeps its own rate limiter at 0.75 req/s.
    • The tier-list disk cache (tier-list-cache.json under %APPDATA%). It held
      scraped pages; the counters artifact keeps its own cache, and two files with
      independent TTLs disagreeing about which was stale is a bug waiting to happen.
      The stale file is simply left unread and can be deleted.

    Fixed

    • CountersArtifactAdapter.fetchChampionStats threw synchronously out of a
      method typed to return a promise, because Promise.resolve(lookup(req))
      evaluates the lookup first. Every caller happened to sit inside a try, so it
      never surfaced — but a failure that arrives as a throw rather than a rejection
      is a trap for the next one.
    Downloads
  • v3.19.0 61e960ba5f

    v3.19.0 - Your Pool This Patch
    All checks were successful
    CI / build-and-test (push) Successful in 2m6s
    Stable

    joeyr released this 2026-08-08 14:06:05 -05:00 | 37 commits to main since this release

    Needs a published-data API key. This section reads the same key as every other artifact — paste it in Settings if you have not already, or it stays on the no-data placeholder.

    Added

    • "Your Pool This Patch" — a third pick list, and the only one that can be on
      screen while you are still deciding.
      Suggested Picks cannot compute anything
      until the enemy laner locks, because Riot never exposes enemy hovers; that is
      most of champ select, and the section spends it showing "waiting for the enemy
      laner". The new list needs only your assigned role, so it fills that window.

      It ranks your configured pool by each champion's own measured win rate at your
      role this patch, from the champion-synergies.json artifact — Riot match data,
      a genuinely independent second opinion from the op.gg tier badge shown beside
      it. Every row carries the games behind it, and the section names the sample it
      came from (NA solo queue, Gold-Emerald), because those are one ladder's numbers
      and you may not be on it.

      Ranking is by a shrunk win rate — pulled toward 50% by games / (games + 400)
      — so 53% over 900 games outranks 56% over 60. The displayed rate stays raw; only
      the ordering is shrunk.

    • Ally pairings are shown as evidence, and deliberately do not rank anything.
      When a teammate hovers or locks, a row can carry a line like 54.7% with Thresh · 192 games for pairs with at least 50 games behind them.

      That restraint is the main design decision in this release, and it is measured
      rather than stylistic. The artifact's headline content is 100k+ ally pairing
      rows, and the obvious feature ranks picks by them. Against the published patch
      16.15 file, each pair's win rate was compared to an additive baseline (the
      champion's own rate plus the ally's own rate minus 50) and the spread of those
      residuals compared to the binomial noise expected if pairing did not matter at
      all — with the null corrected for a pair's games being a subset of both
      champions' totals, which otherwise widens the noise band and manufactures a
      signal:

      Games floor Pairs Observed SD Noise-only SD
      ≥ 20 9,948 8.65 8.65
      ≥ 50 1,828 5.76 5.82
      ≥ 100 200 3.80 4.23

      Zero excess variance at every floor; a 3-point pairing effect would have shown
      as 5.2 in the last row. The consequence is not that the ranking needed better
      tuning — a shrinkage estimator fitted to this data shrinks every pair back to
      its baseline, so the optimal ranker is the one that ignores the rows. Ranking
      on them would have sorted champ-select advice by coin flips, and the top of
      that list is visibly nonsense: a 35-game Lucian+Amumu pair at 77%. Even the ten
      best-sampled pairs in 9.5 MB — Lucian+Nami among them — have confidence
      intervals of ±5 to ±7 points around rates 1-4 points above baseline.

      So the pair line is a measurement with its sample attached, phrased
      symmetrically ("with Thresh", never "thanks to Thresh" — both champions won or
      lost the same games), and it never touches the sort. Live against production:
      Syndra shows the best pairings in a five-champion mid pool and ranks fourth,
      because its own record is 48.4%.

      This is a statement about sample size, not about the game. The producer samples
      ~21k matches per patch and does not carry them across a rollover; if that rises
      roughly 5-7x, re-run the residual test before assuming the finding holds.

    • champion-synergies.json is now consumed — eight of the ten published
      artifacts have a consumer here. It is much the largest at 9.5 MB, which the
      existing machinery absorbed better than expected: 450 KB on the wire (the API
      serves it brotli), ~1 ms to validate under the sampled posture where a strict
      whole-file parse of 103k rows would cost ~125 ms, and 9.2 MB retained — pruned
      to 0.3 MB, since the section reads about 2% of the pair rows.

      It refuses an off-patch copy, matching counters and opposite to damage and
      benchmarks: last patch's win rates are not a degraded answer to "how is this
      champion doing now", they answer a different question. On the morning of a
      patch, before the crawler has caught up, the section goes quiet rather than
      showing yesterday's meta as today's.

      Entries below 50 games are dropped outright rather than kept and filtered at
      the call site — the artifact publishes everything down to games: 1, so that
      judgement had to be made somewhere, and a caller must not be able to rank on a
      two-game record by forgetting a check.

    Downloads