From eb8f275fe248f21a28e263e6c717386cdf10d86b Mon Sep 17 00:00:00 2001 From: Rodrigo Matos Date: Wed, 26 Aug 2026 18:41:50 -0300 Subject: [PATCH] fix(engine): apply minimum-sample floor to weekly origin_distribution (#189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit origin_distribution (which drives weekly AI-adoption %) had no minimum commit count, unlike stabilization_ratio — a single AI-tagged commit in an otherwise quiet week could swing that week to 100% AI adoption. This leaked into the org timeline chart as visible zigzag noise, reported while reviewing #183/#190's connectNulls fix. Now gated behind the same MIN_COMMITS_FOR_RATIO=3 floor stabilization already uses: weeks below it get an empty origin_distribution instead of a distribution computed from too few data points. Downstream, computeOrgTimeline (platform) already treats a week with no origin data as aiWeight=0 -> aiPct=null, and the connectNulls fix already renders null as a gap instead of a fake trend — so this composes with both existing fixes without any platform-side change needed. intent_distribution (feature/fix %) has the same statistical shape but is out of scope here — flagged in #189 as a secondary "worth evaluating" item, not this specific complaint. Co-Authored-By: claude-code_2-1-238_agent --- iris/analysis/activity_timeline.py | 15 ++++-- tests/test_activity_timeline.py | 82 ++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 tests/test_activity_timeline.py diff --git a/iris/analysis/activity_timeline.py b/iris/analysis/activity_timeline.py index cec895d..971e819 100644 --- a/iris/analysis/activity_timeline.py +++ b/iris/analysis/activity_timeline.py @@ -147,11 +147,16 @@ def calculate_activity_timeline( classified = classify_commit(c) intent_dist[classified.intent.value] += 1 - # Origin distribution - origin_dist: dict[str, int] = defaultdict(int) - for c in wc: - origin = classify_origin(c) - origin_dist[origin.value] += 1 + # Origin distribution (only meaningful with enough commits — same + # floor as stabilization; a single AI-tagged commit in an otherwise + # quiet week shouldn't be able to swing weekly AI-adoption to 100%). + origin_dist: dict[str, int] = {} + if total_commits >= MIN_COMMITS_FOR_RATIO: + origin_counts: dict[str, int] = defaultdict(int) + for c in wc: + origin = classify_origin(c) + origin_counts[origin.value] += 1 + origin_dist = dict(origin_counts) # Stabilization and churn (only meaningful with enough commits) stab_ratio = None diff --git a/tests/test_activity_timeline.py b/tests/test_activity_timeline.py new file mode 100644 index 0000000..c2fdb38 --- /dev/null +++ b/tests/test_activity_timeline.py @@ -0,0 +1,82 @@ +"""Tests for activity_timeline's weekly origin-distribution sample floor (#189). + +A week's origin_distribution (which drives weekly AI-adoption %) previously +had no minimum commit count, unlike stabilization_ratio — a single AI-tagged +commit in an otherwise quiet week could swing that week to 100% AI adoption, +a statistically meaningless spike that leaked into the org timeline chart as +visible noise. + +Runnable as: `python -m pytest tests/test_activity_timeline.py -v` +""" + +import sys +from datetime import datetime, timezone +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from iris.analysis.activity_timeline import ( + MIN_COMMITS_FOR_RATIO, + calculate_activity_timeline, +) +from iris.models.commit import Commit + + +def _commit(day: int, attribution_trailers: list[str] | None = None) -> Commit: + return Commit( + hash=f"h{day}", + author="Alice", + date=datetime(2026, 1, day, tzinfo=timezone.utc), + attribution_trailers=attribution_trailers or [], + ) + + +def test_week_below_min_commits_has_empty_origin_distribution(): + # Week A (Jan 1): a single AI-tagged commit — below MIN_COMMITS_FOR_RATIO. + # Week B (Jan 15-17): three human commits — a two-week gap safely clears + # any ISO-week-boundary ambiguity between the two groups. + commits = [ + _commit(1, attribution_trailers=["copilot@users.noreply.github.com"]), + _commit(15), + _commit(16), + _commit(17), + ] + result = calculate_activity_timeline(commits, churn_days=14) + assert result is not None + + week_a = next(w for w in result.weeks if w.commits == 1) + assert week_a.origin_distribution == {} + assert week_a.stabilization_ratio is None + + +def test_week_at_min_commits_has_populated_origin_distribution(): + commits = [ + _commit(1, attribution_trailers=["copilot@users.noreply.github.com"]), + _commit(2), + _commit(3), + _commit(15), + ] + assert len([c for c in commits if c.date.day <= 3]) == MIN_COMMITS_FOR_RATIO + + result = calculate_activity_timeline(commits, churn_days=14) + assert result is not None + + week_a = next(w for w in result.weeks if w.commits == MIN_COMMITS_FOR_RATIO) + assert sum(week_a.origin_distribution.values()) == MIN_COMMITS_FOR_RATIO + assert week_a.origin_distribution.get("AI_ASSISTED") == 1 + + +if __name__ == "__main__": + tests = [fn for name, fn in globals().items() if name.startswith("test_")] + failed = 0 + for fn in tests: + try: + fn() + print(f"ok {fn.__name__}") + except AssertionError: + failed += 1 + print(f"FAIL {fn.__name__}") + if failed: + print(f"\n{failed} failure(s)") + sys.exit(1) + print(f"\n{len(tests)} tests passed")