• v0.28.0 b463527cc1

    v0.28.0
    All checks were successful
    CI / gate (push) Successful in 56s
    CI / image (push) Successful in 24s
    Stable

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

    Changed

    • Benchmarks, counters, synergies and builds are built over both retained
      patches instead of one.
      Retention has kept the current patch and its
      predecessor since the table existed, and every aggregate filtered
      WHERE patch = $1 and walked past rows a night's crawl had already paid for.
      Those four now read WHERE patch = ANY($1::text[]) over the whole window.

      The gain is lopsided in time rather than a flat doubling. The predecessor is a
      finished crawl — ~26,000 matches on 16.15 — while the current patch holds
      whatever the nights since rollover collected: roughly 14x the sample on day
      one, ~4x by day four, ~2x by the end of a patch. On counters, whose median
      matchup cell was two games, that is the difference between publishing noise
      and publishing something a consumer can put a floor under.

      It does not make counters well-sampled. It roughly doubles a median of two.
      MATCH_LOOKBACK_HOURS and the crawl window are where the rest of that
      problem lives.

    • champion-damage and champion-runes deliberately did not change. Damage
      splits are ratios that converge in a few dozen games, and pooling would blur
      the item reworks anyone reads a damage split to find — it keeps its per-entry
      fallback. Runes are the best-sampled artifact here, because a full nine-perk
      page rides free on every match-v5 row, and the fastest-moving under a balance
      patch; they stay on the current patch under either retention setting.

    • RETAIN_PREVIOUS_PATCH=false is now a quality decision, not a storage one.
      It used to cost one thinner morning per patch. It now roughly halves the
      sample behind four artifacts for the whole patch. Its doc comment in
      config.ts, the README's "The two-patch window" and CLAUDE.md all say so.

    • generatedFrom.previousPatch is published on counters, synergies and
      builds.
      Required and nullable here; optional and nullable in lol-companion's
      mirror, which shipped first in 4.2.0 — those schemas are .strict(), so a
      producer that published the field first would have every install reject the
      file and freeze on cached data. generatedFrom.patch is untouched and still
      names the patch the file is for.

      Publishing the window without naming it would have left three files quietly
      claiming a one-patch sample they no longer have.

    • stale on a benchmarks bucket now means "the current patch has measured
      nothing here yet."
      It used to mean "served from the previous patch because
      this one had nothing", which pooling makes meaningless — every bucket may hold
      predecessor rows. The producer reads it off a new currentN column on
      benchmarkRows rather than off which of two queries the row came from, so the
      patch-day signal survives with its meaning intact and lol-companion's existing
      labelling stays correct. The damage artifact's identically named flag still
      marks a genuine either/or fallback; the two are no longer the same signal.

    • Benchmarks reversed "one fresh sample beats a stale thousand" on purpose.
      The fallback preferred a single current-patch row over five thousand beside
      it. A benchmark is a habit, and 5,001 games describe a habit better than the
      one that happens to be on this patch. The pooled mean is weighted by n, so
      the fresh sample counts for exactly what it is worth. The rule still holds for
      damage, and CLAUDE.md now scopes it to that artifact.

      Pooling is also the only lever that reaches a thin bucket without withholding
      data: raising BENCHMARK_MIN_SAMPLES does not improve a three-game bucket, it
      deletes it, and what is never published cannot be recovered downstream.

    • buildOrderCoverage stays on the current patch while the builds artifact
      pools.
      It steers which champions the crawl spends its rationed timeline
      requests on, and counting the predecessor's orders as coverage would starve
      the current patch — leaving the pooled file ever more predecessor-weighted
      with each night.

    Added

    • Nine integration tests for the pooled window, against real SQL, in
      test/integration/store.test.ts. Two of them are the correctness properties
      the change turns on and that no unit test can reach: that the matchup and
      synergy self-joins key on b.patch = a.patch, so a widened window sums two
      independent within-patch tallies rather than fabricating pairs between players
      who were never in a game together; and that synergyRows widens its pair
      numerators and its totals denominators together, since widening only the
      first would halve every rate in the file while still validating.

    Known risk

    • A pooled build order can silently lose an item Riot removed between the two
      patches.
      Purchases are stored raw and classified at build time against
      current Data Dragon, so coreSequence drops an id the catalog no longer
      carries — leaving a hole in a sequence presented as observed. Removals cluster
      at season boundaries, so this is rare, but it fails quietly rather than
      loudly. Documented on RiotBuildsOptions.previousPatch.
    Downloads