Skip to main content
← Back to list
01Issue
BugClosedSwamp Club
Assigneeskeeb

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 — countAuthorPulls reads 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:

  1. Count versions ever-published (ignore yankedAt) for the scoring query specifically, or
  2. 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.

02Bog Flow
OPENTRIAGEDIN PROGRESSCLOSED+ 1 MOREASSIGNED+ 5 MOREREVIEW

Closed

7/17/2026, 12:30:43 AM

No activity in this phase yet.

03Sludge Pulse
keeb assigned keeb6/27/2026, 2:57:56 AM
Editable. Press Enter to edit.

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.