hardTtlFor() and badge.ts treat a soft-deadline partial that carries a firstRelease as terminal, so a truncated traversal's answer is pinned: 30 days in the result cache, 24h at camo.
packages/web/src/resolve.ts:54:
function hardTtlFor(r: LookupResult): number {
if (r.firstRelease) return HARD_TTL_RELEASED; // 30 days
if (r.partial) return HARD_TTL_PARTIAL;
return HARD_TTL_PENDING;
}
isFresh() (same file, :60) has the same ordering — if (entry.value.firstRelease) return true — so such an entry never revalidates. packages/web/src/routes/badge.ts:141 does the same thing at the edge: resolved.result.firstRelease ? LONG_CACHE : SHORT_CACHE, and badgeStateForResult renders the tag rather than checking… whenever firstRelease is set.
Why it matters. A partial means the traversal was truncated by the soft deadline, so its firstRelease is a best-effort answer, not confirmed to be the earliest release containing the commit. Concretely: the gallop finds v2.0.0 and the deadline blows before it can walk back to v1.9.0, which is the true first release. That wrong-but-plausible tag is then treated as final — kept for 30 days in the result cache and served to camo with a 24h TTL — with no path to correction short of the cache entry expiring.
Deadline-heavy repos (gitlab.gnome.org/GNOME/gimp, kubernetes/kubernetes) are exactly where partial is common, so this is not a rare edge.
Where it came from. Found while reviewing #158 (fix(web-og): cache the OG card by terminality, not by result presence). #158 fixes the OG surface — its isTerminal() requires firstRelease != null && !partial, so an OG card for a partial revalidates every 300s. That makes #158's OG lifetime stricter than the web/badge surfaces, and this issue is the remaining half: #151 asked for "cache by whether the answer can still change", and on the result cache + badge it still caches by "did an answer come back".
Sketch of the fix. Test partial before firstRelease in both hardTtlFor() and isFresh(), so a partial-with-release gets HARD_TTL_PARTIAL and revalidates; and key badge's cache lifetime on firstRelease && !partial to match. Needs a deliberate call on the badge copy: a partial-with-release should probably still render the tag it found (better than checking…) but with the short TTL, so it self-corrects.
Not fixed in #158 — that PR touches only packages/web-og, and this is packages/web. Filed separately so the change lands in a PR that is about it.
hardTtlFor()andbadge.tstreat a soft-deadlinepartialthat carries afirstReleaseas terminal, so a truncated traversal's answer is pinned: 30 days in the result cache, 24h at camo.packages/web/src/resolve.ts:54:isFresh()(same file, :60) has the same ordering —if (entry.value.firstRelease) return true— so such an entry never revalidates.packages/web/src/routes/badge.ts:141does the same thing at the edge:resolved.result.firstRelease ? LONG_CACHE : SHORT_CACHE, andbadgeStateForResultrenders the tag rather thanchecking…wheneverfirstReleaseis set.Why it matters. A
partialmeans the traversal was truncated by the soft deadline, so itsfirstReleaseis a best-effort answer, not confirmed to be the earliest release containing the commit. Concretely: the gallop findsv2.0.0and the deadline blows before it can walk back tov1.9.0, which is the true first release. That wrong-but-plausible tag is then treated as final — kept for 30 days in the result cache and served to camo with a 24h TTL — with no path to correction short of the cache entry expiring.Deadline-heavy repos (
gitlab.gnome.org/GNOME/gimp,kubernetes/kubernetes) are exactly wherepartialis common, so this is not a rare edge.Where it came from. Found while reviewing #158 (
fix(web-og): cache the OG card by terminality, not by result presence). #158 fixes the OG surface — itsisTerminal()requiresfirstRelease != null && !partial, so an OG card for a partial revalidates every 300s. That makes #158's OG lifetime stricter than the web/badge surfaces, and this issue is the remaining half: #151 asked for "cache by whether the answer can still change", and on the result cache + badge it still caches by "did an answer come back".Sketch of the fix. Test
partialbeforefirstReleasein bothhardTtlFor()andisFresh(), so a partial-with-release getsHARD_TTL_PARTIALand revalidates; and key badge's cache lifetime onfirstRelease && !partialto match. Needs a deliberate call on the badge copy: a partial-with-release should probably still render the tag it found (better thanchecking…) but with the short TTL, so it self-corrects.Not fixed in #158 — that PR touches only
packages/web-og, and this ispackages/web. Filed separately so the change lands in a PR that is about it.