Context
packages/loopover-miner/lib/opportunity-fanout.js's githubGetJson (lines 123-138) wraps every GitHub API GET
in fetchWithRetry (http-retry.js) specifically so "a transient SERVER error (a 5xx RESPONSE) or a transient
GitHub RATE-LIMIT response... is retried a bounded number of times" rather than being treated as a permanent
signal on the first blip.
packages/loopover-miner/lib/contribution-profile-extract.js's getJson (lines 75-87) is the same shape of
request — a small, bounded, AbortSignal.timeout-guarded GitHub API GET, used by extractContributionProfile
to fetch a repo's labels and CONTRIBUTING.md/AI-USAGE.md-style docs — but it calls fetchImpl directly with
no retry at all: a single 5xx or transient network hiccup makes getJson return null, which the extraction
logic (per this file's own header comment, "any fetch/parse failure degrades a signal to absent/unknown
rather than erroring") folds into a low-confidence ContributionProfile.
This matters more than a typical "missing retry" gap because the degraded result is not just returned once and
discarded — discover-cli.js's resolveContributionProfilesForDiscover (lines 169-187) immediately
cache.put(profile, ctx.nowMs)s whatever extract() returns into contribution-profile-cache.js's persistent
store, keyed by repoFullName with its own freshness TTL (CONTRIBUTION_PROFILE_CACHE_TTL_MS). A transient
GitHub 5xx during one discover run therefore doesn't just degrade that one run's ranking — it gets PERSISTED as
the repo's cached eligibility signal and continues silently disabling (or weakening) contribution-profile- filter.js's label-based eligibility filtering for that repo until the cache entry naturally expires, even
though the repo's real label taxonomy was reachable moments before or after the blip.
Requirements
getJson (packages/loopover-miner/lib/contribution-profile-extract.js) MUST retry a transient GitHub 5xx or
rate-limit response (429 / secondary-403) via fetchWithRetry from ./http-retry.js, matching
opportunity-fanout.js's githubGetJson usage: same retryable-status semantics, same bounded attempts, same
exponential-backoff-or-Retry-After (whichever is longer) delay.
- The existing per-attempt request timeout (
REQUEST_TIMEOUT_MS via AbortSignal.timeout) MUST be preserved —
fetchWithRetry's own timeoutMs option already gives each retry attempt a FRESH timeout, so this is a
straightforward pass-through, not a redesign.
getJson MUST remain "never throws" (its documented contract): the function's return type/behavior for a
non-retryable failure (a plain 4xx, or every retry attempt exhausted) MUST be unchanged — still null, never
a thrown error.
- The new retry MUST be injectable/testable without real timers, mirroring every other
fetchWithRetry call
site's own sleepFn seam.
Deliverables
Test Coverage Requirements
99%+ Codecov patch coverage on every changed line and branch in contribution-profile-extract.js, including the
retry-succeeds and retry-exhausted branches for both the label-list fetch and the doc-content fetch call sites,
plus the two regression tests above using an injected sleepFn (no real timers).
Expected Outcome
A transient GitHub 5xx/rate-limit blip during discover's contribution-profile extraction no longer gets
permanently cached as a degraded, low-confidence eligibility signal for that repo — contribution-profile- extract.js rides out the same class of transient failure opportunity-fanout.js's sibling fetcher already
does, before falling back to its existing, unchanged absent/unknown degradation once retries are genuinely
exhausted.
Links & Resources
packages/loopover-miner/lib/contribution-profile-extract.js:75-87 (getJson) — the code to change.
packages/loopover-miner/lib/opportunity-fanout.js:123-138 (githubGetJson) — the sibling fetcher already
using fetchWithRetry for the identical class of request.
packages/loopover-miner/lib/http-retry.js (fetchWithRetry) — the shared retry helper to adopt.
packages/loopover-miner/lib/contribution-profile-cache.js, discover-cli.js's
resolveContributionProfilesForDiscover — where a degraded profile gets persisted and why the caching makes
this worth fixing beyond a single run.
Context
packages/loopover-miner/lib/opportunity-fanout.js'sgithubGetJson(lines 123-138) wraps every GitHub API GETin
fetchWithRetry(http-retry.js) specifically so "a transient SERVER error (a 5xx RESPONSE) or a transientGitHub RATE-LIMIT response... is retried a bounded number of times" rather than being treated as a permanent
signal on the first blip.
packages/loopover-miner/lib/contribution-profile-extract.js'sgetJson(lines 75-87) is the same shape ofrequest — a small, bounded,
AbortSignal.timeout-guarded GitHub API GET, used byextractContributionProfileto fetch a repo's labels and
CONTRIBUTING.md/AI-USAGE.md-style docs — but it callsfetchImpldirectly withno retry at all: a single 5xx or transient network hiccup makes
getJsonreturnnull, which the extractionlogic (per this file's own header comment, "any fetch/parse failure degrades a signal to
absent/unknownrather than erroring") folds into a low-confidence
ContributionProfile.This matters more than a typical "missing retry" gap because the degraded result is not just returned once and
discarded —
discover-cli.js'sresolveContributionProfilesForDiscover(lines 169-187) immediatelycache.put(profile, ctx.nowMs)s whateverextract()returns intocontribution-profile-cache.js's persistentstore, keyed by
repoFullNamewith its own freshness TTL (CONTRIBUTION_PROFILE_CACHE_TTL_MS). A transientGitHub 5xx during one
discoverrun therefore doesn't just degrade that one run's ranking — it gets PERSISTED asthe repo's cached eligibility signal and continues silently disabling (or weakening)
contribution-profile- filter.js's label-based eligibility filtering for that repo until the cache entry naturally expires, eventhough the repo's real label taxonomy was reachable moments before or after the blip.
Requirements
getJson(packages/loopover-miner/lib/contribution-profile-extract.js) MUST retry a transient GitHub 5xx orrate-limit response (429 / secondary-403) via
fetchWithRetryfrom./http-retry.js, matchingopportunity-fanout.js'sgithubGetJsonusage: same retryable-status semantics, same bounded attempts, sameexponential-backoff-or-
Retry-After(whichever is longer) delay.REQUEST_TIMEOUT_MSviaAbortSignal.timeout) MUST be preserved —fetchWithRetry's owntimeoutMsoption already gives each retry attempt a FRESH timeout, so this is astraightforward pass-through, not a redesign.
getJsonMUST remain "never throws" (its documented contract): the function's return type/behavior for anon-retryable failure (a plain 4xx, or every retry attempt exhausted) MUST be unchanged — still
null, nevera thrown error.
fetchWithRetrycallsite's own
sleepFnseam.Deliverables
getJsoninpackages/loopover-miner/lib/contribution-profile-extract.jsretries transient 5xx/rate-limitresponses via
fetchWithRetryinstead of returningnullon the first such response.later attempt (within the retry budget) produces the SAME
ContributionProfileconfidence/signals as animmediate success, not a degraded
absent/unknownresult.absent/unknownexactly astoday (never throws), preserving the existing fail-open contract once retries are genuinely exhausted.
Test Coverage Requirements
99%+ Codecov patch coverage on every changed line and branch in
contribution-profile-extract.js, including theretry-succeeds and retry-exhausted branches for both the label-list fetch and the doc-content fetch call sites,
plus the two regression tests above using an injected
sleepFn(no real timers).Expected Outcome
A transient GitHub 5xx/rate-limit blip during
discover's contribution-profile extraction no longer getspermanently cached as a degraded, low-confidence eligibility signal for that repo —
contribution-profile- extract.jsrides out the same class of transient failureopportunity-fanout.js's sibling fetcher alreadydoes, before falling back to its existing, unchanged
absent/unknowndegradation once retries are genuinelyexhausted.
Links & Resources
packages/loopover-miner/lib/contribution-profile-extract.js:75-87(getJson) — the code to change.packages/loopover-miner/lib/opportunity-fanout.js:123-138(githubGetJson) — the sibling fetcher alreadyusing
fetchWithRetryfor the identical class of request.packages/loopover-miner/lib/http-retry.js(fetchWithRetry) — the shared retry helper to adopt.packages/loopover-miner/lib/contribution-profile-cache.js,discover-cli.js'sresolveContributionProfilesForDiscover— where a degraded profile gets persisted and why the caching makesthis worth fixing beyond a single run.