From d469f076c2a2f0dd1b50feb8826a09303ce7e322 Mon Sep 17 00:00:00 2001 From: Franz Hemmer Date: Mon, 19 Oct 2026 01:56:33 -0400 Subject: [PATCH 1/2] fix: use repository language metadata instead of churn (#80) --- README.md | 9 ++++---- scripts/generate_fixture.py | 12 ++++++++++ src/analyzer.py | 9 ++------ tests/browser/report.spec.js | 14 +++++++++++ tests/test_analyzer.py | 45 ++++++++++++++++++++++++++++++++++-- 5 files changed, 76 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f699f8a..7e79541 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,11 @@ the selected table. Keyboard focus has a visible outline. - **Auditing filters**: Searchable and filterable tables for repositories, recent pull requests, and commit logs with links directly to GitHub. - **Arbitrary timeframes**: Audit any historical period using `--weeks ` or explicit `--start YYYY-MM-DD` and `--end YYYY-MM-DD` dates. -Repository profile language follows the largest file-churn total in the selected -interval. Without file-derived language, it uses GitHub's primary language. -Missing, null, or empty language metadata displays `Unknown`, including repositories -whose commits change no files. +Repository profile language uses GitHub's primary language for the repository, +independent of file churn in the selected interval. Missing, null, or empty +language metadata displays `Unknown`; recent changes alone do not establish a +repository's implementation language. Lockfile activity remains included in +commit, line-churn, and file-type breakdown totals. PR cycle-time medians use merged PRs with a known creation-to-merge duration. Even-sized samples average the middle two values; odd-sized samples use the diff --git a/scripts/generate_fixture.py b/scripts/generate_fixture.py index 0340b24..cc135c5 100644 --- a/scripts/generate_fixture.py +++ b/scripts/generate_fixture.py @@ -33,6 +33,18 @@ def main(): build_html_report(analyze_data(organization)), encoding="utf-8" ) + languages = load_report_fixture() + languages["repo_meta"]["hs-buddy"]["primaryLanguage"] = {"name": "TypeScript"} + commit = languages["commits"][0] + commit["files"].append({"path": "uv.lock", "language": "Lockfile", "adds": 10000, "dels": 5000}) + commit["additions"] += 10000 + commit["deletions"] += 5000 + commit["net_lines"] += 5000 + languages["commits"].append(dict(commit, repo="unknown-language", hash="e" * 40)) + (output / "report-languages.html").write_text( + build_html_report(analyze_data(languages)), encoding="utf-8" + ) + # Exercise the directory with repeated editions, long names and actual local targets. site = output / "landing" site.mkdir(exist_ok=True) diff --git a/src/analyzer.py b/src/analyzer.py index a1b8bb3..b00d23a 100644 --- a/src/analyzer.py +++ b/src/analyzer.py @@ -243,13 +243,8 @@ def analyze_data(data): r_dels = sum(c["deletions"] for c in r_commits) r_net = r_adds - r_dels - # Languages in this repo - r_langs = Counter() - for c in r_commits: - for f in c.get("files", []): - r_langs[f["language"]] += f["adds"] + f["dels"] - metadata_language = (meta.get("primaryLanguage") or {}).get("name") or "Unknown" - primary_lang = r_langs.most_common(1)[0][0] if r_langs else metadata_language + # Repository language describes the project, not its recent file churn. + primary_lang = (meta.get("primaryLanguage") or {}).get("name") or "Unknown" r_active_days = len(set(c["date_str"] for c in r_commits)) first_commit = min((c["author_date"] for c in r_commits), default=None) diff --git a/tests/browser/report.spec.js b/tests/browser/report.spec.js index a8c6618..870f25c 100644 --- a/tests/browser/report.spec.js +++ b/tests/browser/report.spec.js @@ -43,3 +43,17 @@ test('organization repository and commit links retain canonical owner alongside await expect(page.locator('#pane-commits a[href^="https://github.com/HemSoft/retained/commit/"]')).toHaveCount(1); await expect(page.locator('#pane-commits a[href^="https://github.com/HemSoft/hs-buddy/commit/"]')).toHaveCount(0); }); + +test('repository languages use metadata despite dominant lockfile churn', async ({ page }) => { + await openReport(page, { reportPath: path.join(root, 'test-results/report-languages.html') }); + const known = page.locator('#table-repos tbody tr').filter({ has: page.getByRole('link', { name: 'hs-buddy', exact: true }) }); + await expect(known.locator('td').nth(2)).toHaveText('TypeScript'); + await expect(known.locator('td').nth(3)).toHaveText('2'); + await expect(known.locator('td').nth(5)).toHaveText('+10,110'); + await expect(known.locator('td').nth(6)).toHaveText('-5,025'); + const unknown = page.locator('#table-repos tbody tr').filter({ has: page.getByRole('link', { name: 'unknown-language', exact: true }) }); + await expect(unknown.locator('td').nth(2)).toHaveText('Unknown'); + await expect(page.locator('#table-repos')).not.toContainText('Lockfile'); + await page.getByRole('heading', { name: 'Repository deep dive', exact: true }).scrollIntoViewIfNeeded(); + await page.screenshot({ path: path.join(root, 'test-results/repository-languages.png') }); +}); diff --git a/tests/test_analyzer.py b/tests/test_analyzer.py index 0573123..623e4f7 100644 --- a/tests/test_analyzer.py +++ b/tests/test_analyzer.py @@ -147,14 +147,55 @@ def test_empty_commit_profiles_handle_missing_null_and_empty_language(self): self.assertEqual(analysis["repo_profiles"][0]["commits"], 1) self.assertIn("Unknown", build_html_report(analysis)) - def test_profile_language_uses_churn_then_metadata(self): + def test_profile_language_uses_repository_metadata(self): data = copy.deepcopy(self.sample_data) data["repo_meta"]["hs-buddy"]["primaryLanguage"] = {"name": "Python"} - self.assertEqual(analyze_data(data)["repo_profiles"][0]["primary_language"], "TypeScript") + self.assertEqual(analyze_data(data)["repo_profiles"][0]["primary_language"], "Python") for commit in data["commits"]: commit.update(files=[], additions=0, deletions=0, net_lines=0) self.assertEqual(analyze_data(data)["repo_profiles"][0]["primary_language"], "Python") + def test_lockfile_churn_does_not_replace_repository_language_or_activity(self): + data = copy.deepcopy(self.sample_data) + data["repo_meta"]["hs-buddy"]["primaryLanguage"] = {"name": "TypeScript"} + commit = data["commits"][0] + commit["files"].append( + {"path": "uv.lock", "language": "Lockfile", "adds": 10000, "dels": 5000} + ) + commit["additions"] += 10000 + commit["deletions"] += 5000 + commit["net_lines"] += 5000 + analysis = analyze_data(data) + profile = analysis["repo_profiles"][0] + self.assertEqual(profile["primary_language"], "TypeScript") + self.assertEqual(profile["commits"], 2) + self.assertEqual(profile["additions"], 10110) + self.assertEqual(profile["deletions"], 5025) + self.assertEqual(analysis["kpis"]["total_additions"], 10110) + self.assertEqual(analysis["kpis"]["total_deletions"], 5025) + lockfile = next( + row for row in analysis["language_breakdown"] if row["language"] == "Lockfile" + ) + self.assertEqual(lockfile["additions"], 10000) + self.assertEqual(lockfile["deletions"], 5000) + + def test_missing_repository_language_does_not_infer_from_interval_churn(self): + for metadata in ( + {}, + {"primaryLanguage": None}, + {"primaryLanguage": {}}, + {"primaryLanguage": {"name": ""}}, + ): + with self.subTest(metadata=metadata): + data = copy.deepcopy(self.sample_data) + data["repo_meta"]["hs-buddy"] = metadata + data["commits"][0]["files"].append( + {"path": "uv.lock", "language": "Lockfile", "adds": 10000, "dels": 0} + ) + self.assertEqual( + analyze_data(data)["repo_profiles"][0]["primary_language"], "Unknown" + ) + def test_median_cycle_time_for_even_odd_empty_and_single_samples(self): cases = [ ([0.5, 3.5], 2.0), From b0a7be6d84aac2d9225f61acee021b65214e505c Mon Sep 17 00:00:00 2001 From: Franz Hemmer Date: Mon, 19 Oct 2026 01:57:37 -0400 Subject: [PATCH 2/2] test: capture repository language regression at the table --- tests/browser/report.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/report.spec.js b/tests/browser/report.spec.js index 870f25c..e5d7446 100644 --- a/tests/browser/report.spec.js +++ b/tests/browser/report.spec.js @@ -54,6 +54,6 @@ test('repository languages use metadata despite dominant lockfile churn', async const unknown = page.locator('#table-repos tbody tr').filter({ has: page.getByRole('link', { name: 'unknown-language', exact: true }) }); await expect(unknown.locator('td').nth(2)).toHaveText('Unknown'); await expect(page.locator('#table-repos')).not.toContainText('Lockfile'); - await page.getByRole('heading', { name: 'Repository deep dive', exact: true }).scrollIntoViewIfNeeded(); + await page.locator('#pane-repos').scrollIntoViewIfNeeded(); await page.screenshot({ path: path.join(root, 'test-results/repository-languages.png') }); });