Skip to content

Commit cadd7bc

Browse files
fix: ratio-based issue scoring — popular projects no longer penalized
Before: absolute issue count / 4 (React with 1000 issues = score 0 on this dimension) After: issues / stars ratio (React 1000/200k = 0.005 ratio = full score) This fixes the most critical scoring bug — popular healthy projects like React, Angular, Vue were incorrectly penalized for having many open issues. A project with 1000 issues and 200k stars is healthier than one with 50 issues and 30 stars. Risk dimension also uses ratio: issueBacklogRatio > 1.0 = -3, > 0.5 = -1.5 Tests: 32 passing (added ratio-based scoring test for popular projects) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e88d850 commit cadd7bc

File tree

2 files changed

+37
-12
lines changed

2 files changed

+37
-12
lines changed

cli/lib/scoring.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,21 @@ function computeScore(info) {
3434
const pushRecency = decayScore(daysSincePush, 180);
3535

3636
const openIssues = info.openIssues || 0;
37-
// Stricter curve: 10 issues = 7.5, 30 issues = 2.5, 40+ = 0
38-
const issueRatio = openIssues > 0
39-
? Math.max(parseFloat((10 - Math.min(openIssues / 4, 10)).toFixed(2)), 0)
40-
: 10;
37+
const stars = info.stars || 0;
38+
// Ratio-based: issues relative to community size (stars).
39+
// React (200 issues / 230k stars = 0.0009 ratio) = healthy.
40+
// Small lib (200 issues / 50 stars = 4.0 ratio) = abandoned.
41+
// Ratio > 0.1 starts penalizing, > 1.0 = zero score.
42+
let issueRatio;
43+
if (openIssues === 0) {
44+
issueRatio = 10;
45+
} else if (stars > 0) {
46+
const ratio = openIssues / stars;
47+
issueRatio = Math.max(parseFloat((10 - Math.min(ratio / 0.1 * 10, 10)).toFixed(2)), 0);
48+
} else {
49+
// No stars data — fall back to absolute count (lenient curve)
50+
issueRatio = Math.max(parseFloat((10 - Math.min(openIssues / 10, 10)).toFixed(2)), 0);
51+
}
4152

4253
const publishRecency = decayScore(info.daysSincePublish || 9999, 365);
4354

@@ -57,8 +68,10 @@ function computeScore(info) {
5768
if (daysSincePush > 365) riskBase -= 4;
5869
else if (daysSincePush > 180) riskBase -= 2;
5970

60-
if (openIssues > 100) riskBase -= 3;
61-
else if (openIssues > 50) riskBase -= 1.5;
71+
// Issue backlog risk: ratio-based (issues / max(stars, 1))
72+
const issueBacklogRatio = openIssues / Math.max(stars, 1);
73+
if (issueBacklogRatio > 1.0) riskBase -= 3;
74+
else if (issueBacklogRatio > 0.5) riskBase -= 1.5;
6275

6376
if (info.daysSincePublish > 730) riskBase -= 2;
6477
else if (info.daysSincePublish > 365) riskBase -= 1;

cli/test/scoring.test.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,18 +183,30 @@ describe('computeScore', () => {
183183
`With license (${withLicense.health_score}) should score higher than no license (${noLicense.health_score})`);
184184
});
185185

186-
it('stricter issue scoring: 20 issues should penalize significantly', () => {
186+
it('ratio-based issue scoring: small project with many issues penalized', () => {
187187
const clean = computeScore({
188-
stars: 100, forks: 10, openIssues: 0, downloads: 1000,
188+
stars: 50, forks: 5, openIssues: 0, downloads: 1000,
189189
daysSincePush: 30, daysSincePublish: 60,
190190
deprecated: false, archived: false, license: 'MIT'
191191
});
192-
const issues20 = computeScore({
193-
stars: 100, forks: 10, openIssues: 20, downloads: 1000,
192+
const overloaded = computeScore({
193+
stars: 50, forks: 5, openIssues: 50, downloads: 1000,
194194
daysSincePush: 30, daysSincePublish: 60,
195195
deprecated: false, archived: false, license: 'MIT'
196196
});
197-
const diff = clean.health_score - issues20.health_score;
198-
assert.ok(diff > 3, `20 open issues should drop score by >3 points (actual: ${diff.toFixed(1)})`);
197+
const diff = clean.health_score - overloaded.health_score;
198+
assert.ok(diff > 3, `50 issues on 50-star project should penalize (actual: ${diff.toFixed(1)})`);
199+
});
200+
201+
it('ratio-based issue scoring: popular project with many issues not penalized', () => {
202+
// React-like: 200k stars, 1000 issues = ratio 0.005 = healthy
203+
const result = computeScore({
204+
stars: 200000, forks: 40000, openIssues: 1000, downloads: 50000000,
205+
daysSincePush: 1, daysSincePublish: 7,
206+
deprecated: false, archived: false, license: 'MIT'
207+
});
208+
assert.ok(result.health_score >= 70,
209+
`Popular project with 1000 issues should still score 70+ (got ${result.health_score})`);
210+
assert.equal(result.risk_level, 'healthy');
199211
});
200212
});

0 commit comments

Comments
 (0)