Skip to content

fix(stats): round global prCount at the per-year boundary - #71

Open
detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-stats-round-global-prcount-at-the-per-year-bou-d058eb
Open

detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-stats-round-global-prcount-at-the-per-year-bou-d058eb

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Detail bug report: View on Detail

Bug

The unauthenticated statOutcomes (global) and statOutcomesByYear (per-year breakdown) endpoints both return the same StatOutcomes type with an Int prCount, but they rounded that field at different aggregation boundaries:

  • Global: Math.round(Σ_all_years fractional_sum) — one Math.round on the aggregate.
  • Per-year: Math.round(fractional_sum) per year-group, leaving callers to sum.

For pre-2025 years prCount uses the fractional estimator PRE_2025_PR_RATE = 0.71, so round(Σ f_i) ≠ Σ round(f_i) once pre-2025 events span ≥2 year-groups. Concrete case: two pre-2025 years with 1 matched project each → per-year gives round(0.71)+round(0.71)=2, global gave round(1.42)=1. The two views of the same field could disagree, with no in-repo consumer to notice. Introduced when the endpoints were added in a18bd13.

Fix

Round prCount at the same per-year-group boundary in both endpoints, so the global value is definitionally equal to the sum of the breakdown:

  • Extracted the per-year GROUP BY query into a single shared projectsByYearQuery() used by both computeStatOutcomes and computeStatOutcomesByYear. The standalone global-aggregate prCount SQL is removed.
  • The global prCount is now projectsByYear.reduce((acc, y) => acc + Math.round(y.prCount), 0) — the sum of the already-rounded per-year values.

Sharing the query structurally prevents the two rounding boundaries from drifting apart again, and the reduce (vs. the old prCount[0].prCount indexing) is robust to an empty GROUP BY result.

Testing

  • Offline unit tests (src/resolvers/Stats.test.ts): added a regression guard, run with npx ts-node --transpile-only. It stubs Prisma and calls the private compute* methods directly to bypass the 1-hour ttlCache. It verifies the headline divergent case (two pre-2025 years → global prCount is 2, not 1), the empty/no-matched-project path returns 0 without crashing, that all StatOutcomes fields reconcile globally vs. the sum of the per-year view, and the structural invariant that both endpoints issue exactly one shared prCount-shaped query. Reverting the fix makes the suite fail; restoring makes it pass.
  • Typecheck (npx tsc --skipLibCheck --noEmit) passes clean.
  • Existing resolver test (src/resolvers/Review.test.ts) still passes — shared test infrastructure (env seeding, typedi Container, decorator metadata) is undisturbed.
  • Lint: npx eslint on the changed files fails with a DeprecationError: 'originalKeywordKind' parser error, but this is a pre-existing, repo-wide incompatibility between the pinned eslint@7 and the installed typescript@5.2.2 — it reproduces identically on the unchanged Review.test.ts. No new source-level findings are introduced.
  • End-to-end against live PostgreSQL (not versioned; temporary scaffolding, since removed): stood up PostgreSQL 16 in a Docker container and applied the schema via npx prisma db push --force-reset (prisma migrate deploy fails on a pre-existing, unrelated migration-ordering bug in the repo). Seeded Events for 2022, 2023, and 2025 with MATCHED projects (2025 with 3/5 carrying a prUrl), started a fresh GraphQL server process per variant, and issued a single unauthenticated query against both endpoints over HTTP. The populated seed returned statOutcomes.prCount = 5 reconciling with per-year 1+1+3 = 5 (the old aggregate behavior would have given round(4.42) = 4); an empty-seed variant returned prCount = 0 and statOutcomesByYear = [] without error. The resolver-level e2e was also confirmed to fail against the unfixed code and pass with the fix.

Automatic Fixes PRs can be configured here.

@augmentcode

augmentcode Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor
🤖 Augment PR Summary

Summary: This PR fixes an inconsistency between the global and yearly outcome statistics.

Changes:

  • Extracts the matched-project aggregation into a shared projectsByYearQuery().
  • Uses that grouped result in both statOutcomes and statOutcomesByYear.
  • Changes global prCount to sum each year's individually rounded PR estimate.
  • Preserves direct PR URL counting for 2025 onward and the 0.71 estimate for earlier years.
  • Handles an empty grouped project result with a zero-initialized reduction.
  • Adds offline resolver tests for fractional rounding, empty data, field reconciliation, and shared-query usage.

Technical Notes: The change makes the global prCount definitionally equal to the sum of the by-year values, avoiding differences caused by rounding before versus after aggregation.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. No suggestions at this time.

Comment augment review to trigger a new review at any time.

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