Relationships
#850 Extension publish score is non-monotonic: yanking versions lowers a user's score
Opened by keeb · 6/27/2026
Summary
computeExtensionScore's publish term is non-monotonic: yanking extension versions lowers a user's score. This conflicts with the monotonic-ledger goal (scores should only ever accumulate).
Root cause
The publish input, countVersionsByUser (lib/infrastructure/mongo-extension-repository.ts), excludes yanked versions:
async countVersionsByUser(userId: string): Promise<number> {
const notYanked = { yankedAt: { $in: [null, undefined] } };
return await this.versions.countDocuments({ pushedByUserId: userId, ...notYanked }, AGG_OPTS);
}Publish points = versionCount * POINTS_PER_PUBLISH (500/version). Yank a version and the count — and the score — drops. Example: 20 versions = 10,000 pts -> yank 5 -> 15 versions = 7,500 pts (loses 2,500).
Why it surfaced now
The pull/publish caps in lib/domain/scoring/extension-score.ts were removed (MAX_PUBLISH_SCORE = 5000). The cap was incidentally masking this: any author with >10 versions sat at the 5,000 ceiling, so yanking from 20->15 versions (10,000->7,500 raw) was invisible — both clamped to 5,000.
The cap removal did NOT create this bug; it unmasked a latent non-monotonicity that already existed in the formula.
Scope
- Pulls are unaffected —
countAuthorPullsreads append-only pull events; monotonic up. - Only the publish term regresses, only for authors who both had >10 versions AND yank. Bounded at 500/version.
Suggested fix
Make the publish input monotonic for scoring, independent of yank state. Options:
- Count versions ever-published (ignore
yankedAt) for the scoring query specifically, or - Track a per-user high-water mark of version count.
Note yank still needs to hide versions everywhere else (listings, registry) — only the scoring count should be yank-agnostic.
Verification
scripts/verify-uncapped-extension-score.ts (deno task verify:ext-score) proves pulls/publishes are uncapped, but does not yet assert publish-term monotonicity under yank. A regression check belongs there once the input is fixed.
Closed
No activity in this phase yet.
keeb commented 6/27/2026, 3:11:43 AM
Deferred to #847 (monotonic-ledger + ClickHouse epic). This non-monotonic publish-score bug is a concrete symptom of the same recompute-from-counts model #847 replaces with an append-only score_grants ledger — fixing #847 makes this impossible by construction, so #850 is subsumed rather than shipped standalone.
Root cause (for #847 to absorb): countVersionsByUser (lib/infrastructure/mongo-extension-repository.ts) excludes yanked versions; that count feeds computeExtensionScore → publishScore, and UserScore.contribute() overwrites the bucket, so a recompute after a yank strictly lowers the stored total. Currently masked on main by MAX_PUBLISH_SCORE=5000 (cap removal #763 was reverted) — it surfaces the moment caps are removed again.
Agreed remediation (forward-compatible with the ledger): rename countVersionsByUser → countPublishedVersionsByUser, drop the yank filter (count versions ever published; the count is scoring-only and versions are append-only), and guard it with a real-Mongo scripts/ harness. De-defer trigger: any scheduled publish-cap removal before #847 lands.
keeb commented 6/28/2026, 8:18:02 PM
Re-anchored to the course-corrected plan (see #847). The forward per-event model fixes this by construction: each publish is an immutable +500 grant keyed name@version, so a yank can't remove a banked grant, and genesis freezes the legacy standing. It stays live on main until the read path flips, so it resolves when the genesis cutover (#877) ships. Carry a yank-never-lowers regression test into that work. Caps are now dropped forward, so the original de-defer trigger is moot.
Sign in to post a ripple.