Skip to content

fix(statistics): credit deaths from the authoritative artifact counter, not stale parser_events - #20

Closed
Afgan0r wants to merge 1 commit into
masterfrom
fix/f13a-deaths-debug
Closed

Afgan0r wants to merge 1 commit into
masterfrom
fix/f13a-deaths-debug

Conversation

@Afgan0r

@Afgan0r Afgan0r commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Problem

The deaths recalc reads the per-player death counter signal from the parser_events table, but the bulk full-run (full-run-recalculation.ts) never re-persists parser_events from raw_snapshot — only the single-replay audit path does. For ~90% of current sg replays (1850 / 2059 with artifact d players) parser_events has 0 player_counter rows while raw_snapshot carries the compact counters, so the counter death path finds nothing.

Player resolution and games are built from raw_snapshot, so the game is counted, while a death with no independent victim kill-row (null-killer / suicide / environmental / bleed-out d>0) is dropped. This is the platform-wide ~11024 death-game undercount (sg all-time 282720 vs the parser's own 293744; exemplar Zero 621 vs 653).

Root cause — confirmed by instrumentation on real staging data

.planning/quick/260615-f13b-deaths-undercount/instrument.ts runs the real repository/service code (buildPlayerIdentityIndex, bestPlayerIdentityIndexed, playerIdentityMatchPriority, calculatePlayerAndSquadAggregates) on real exported rows. Across 16 of Zero's d>0 replays he resolves in all 16 — refuting the F12-timestamp / isNicknameActive hypothesis (he matches via the time-ungated display_name priority-1 branch even for pre-2026-05-10 replays) — yet pre-fix only 6 deaths are credited (3 counter, 3 victim) and 10 are dropped because parser_events lacks a counter event and no kill-row names his eid as victim. Post-fix: 16/16, 0 dropped.

Concrete dropped replay: 9eb1a2ad-… (2021-05-14), prid 3662f461-…; raw_snapshot Zero = {d:1, nkd:1, eid:203}; parser_events = 140 kill / 18 destroyed_vehicle / 6 teamkill / 1 diagnostic, 0 player_counter; no kill-row with victim_entity_id=203. (Diagnostics in findings.sql.)

Fix

Read the death counter from the authoritative raw_snapshot — the same source already used for player resolution and games — instead of the stale events table.

  • service.ts: AggregatePlayerEvidence gains optional counterDeaths?: DeathStats; new tallyArtifactCounterDeaths folds it into the per-replay scratch (player + squad). Exported artifactCounterDeaths(d, td) builds the DeathStats with the same semantics as the event-based counterDeaths (total = max(d, td), by_teamkills = td); counterDeaths now delegates to it.
  • repository.ts: resolvedPlayers attaches counterDeaths = artifactCounterDeaths(player.d, player.td).

Invariants preserved

  • One-life cap (foldCappedDeaths): ≤1 total and ≤1 by_teamkills per replay — additive with the event paths, so overlap collapses to 1 (no inflation).
  • by_teamkills ≤ total (the max cap).
  • Games unaffected; parser/OCAP contract, schema, and API untouched.

Tests

  • aggregates.test.ts: 4 new unit cases (3 fail without the fix).
  • postgres.test.ts: 1 integration case — raw_snapshot d:1, no counter event, no victim row → death credited through the real DB.

Gates

  • pnpm lint, pnpm typecheck, pnpm format, pnpm test (661 unit, +4) — green.
  • service.ts 100% unit coverage. The full pnpm test:coverage gate needs Docker/testcontainers (unavailable in the authoring env); the repository.ts change is covered by the new integration test, which runs under Docker in CI.

🤖 Generated with Claude Code

…r, not stale parser_events

The deaths recalc read the per-player death counter from the `parser_events`
table, which the bulk full-run never re-persists from `raw_snapshot` (only the
single-replay audit path does). For ~90% of current sg replays parser_events has
zero `player_counter` rows while `raw_snapshot` carries the compact counters, so
the counter death path found nothing. Player resolution and games are built from
`raw_snapshot`, so the game was counted while a death with no independent victim
kill-row (null-killer / suicide / environmental / bleed-out d>0) was dropped —
the platform-wide ~11024 death-game undercount (sg all-time 282720 vs the
parser's own 293744; exemplar Zero 621 vs 653).

Confirmed by instrumenting the real repository/service code on real staging rows
(.planning/quick/260615-f13b-deaths-undercount/instrument.ts): across 16 of
Zero's d>0 replays he resolves in all 16 (refuting the nickname-active / F12
hypothesis — he matches via the time-ungated display_name branch), yet pre-fix
only 6 deaths are credited and 10 drop because parser_events lacks a counter
event and no kill-row names his eid as victim.

Fix: read the death counter from the authoritative `raw_snapshot` (the same
source already used for resolution and games). `resolvedPlayers` attaches
`counterDeaths = artifactCounterDeaths(player.d, player.td)`; the service folds it
into the per-replay scratch alongside the event paths. The one-life cap
(foldCappedDeaths, <=1 death and <=1 by_teamkills per replay) is unchanged, so the
addition is collapse-safe (no inflation) and by_teamkills <= total holds. Games,
parser/OCAP contract, schema, and API are untouched.

Tests: 4 unit cases in aggregates.test.ts (3 red without the fix) and 1
integration case in postgres.test.ts (raw_snapshot d:1, no counter event, no
victim row -> death credited through the real DB). service.ts is 100% unit
coverage; the repository change is covered by the integration test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Afgan0r

Afgan0r commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by a clean PR (fix/deaths-counter-from-artifact) — #20 inadvertently reverted the Sentry feature (#19). Same deaths fix, Sentry preserved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant