From bf5fa7dcbd489d73dc0fe8532761fab402d20680 Mon Sep 17 00:00:00 2001 From: real-venus Date: Wed, 22 Jul 2026 16:21:48 +0200 Subject: [PATCH] refactor(services): rename burden-forecast loader to reflect cache-only behavior (#8019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadOrComputeBurdenForecastResponse only ever returns a stored snapshot or null — the inline compute fallback was removed in #906 (moved to the background buildBurdenForecasts job) but the name, the unreachable 'computed' source type variant, and the docs were left behind. Rename to loadCachedBurdenForecastResponse across all call sites, drop the dead 'computed' variant from BurdenForecastResponse.source (the code only ever sets 'snapshot'), and document the real snapshot-or-null behavior. --- src/api/routes.ts | 4 ++-- src/mcp/server.ts | 4 ++-- src/services/burden-forecast.ts | 11 +++++++++-- test/unit/burden-forecast.test.ts | 20 ++++++++++---------- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/api/routes.ts b/src/api/routes.ts index 4061e37265..8055f95372 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -241,7 +241,7 @@ import { generateAndSendReviewRecap } from "../services/review-recap"; import { loadOrComputeIssueQualityResponse } from "../services/issue-quality"; import { loadMaintainerNoiseReport } from "../services/maintainer-noise"; import { buildAmsMinerCohortComparison } from "../review/ams-miner-cohort"; -import { loadOrComputeBurdenForecastResponse } from "../services/burden-forecast"; +import { loadCachedBurdenForecastResponse } from "../services/burden-forecast"; import { buildUnavailableQueueTrendReport } from "../services/queue-trends"; import { loadOrComputeRepoOutcomePatternsResponse } from "../services/repo-outcome-patterns"; import { PREFLIGHT_LIMITS } from "../signals/preflight-limits"; @@ -5719,7 +5719,7 @@ async function buildRepoIntelligenceResponse(env: Env, fullName: string) { ]), ), loadRepoDataQuality(env, fullName), - loadOrComputeBurdenForecastResponse(env, fullName).catch((error) => { + loadCachedBurdenForecastResponse(env, fullName).catch((error) => { burdenForecastError = error; return null; }), diff --git a/src/mcp/server.ts b/src/mcp/server.ts index a8979c8915..f6799b776c 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -104,7 +104,7 @@ import { buildRemediationPlan } from "../services/remediation-plan"; import { deriveEligibilityPlan } from "../services/eligibility-plan"; import { explainScoreBreakdown } from "../services/score-breakdown"; import { loadOrComputeIssueQualityResponse } from "../services/issue-quality"; -import { loadOrComputeBurdenForecastResponse } from "../services/burden-forecast"; +import { loadCachedBurdenForecastResponse } from "../services/burden-forecast"; import { buildMcpClientTelemetry } from "../services/client-telemetry"; import { loadOrComputeRepoOutcomePatternsResponse } from "../services/repo-outcome-patterns"; import { buildRepoOutcomeCalibration, outcomeCalibrationSummary } from "../services/outcome-calibration"; @@ -3455,7 +3455,7 @@ export class LoopoverMcp { private async getBurdenForecast(input: { owner: string; repo: string }): Promise { const fullName = `${input.owner}/${input.repo}`; await this.requireRepoAccess(fullName); - const response = await loadOrComputeBurdenForecastResponse(this.env, fullName); + const response = await loadCachedBurdenForecastResponse(this.env, fullName); if (!response) { return { summary: `LoopOver has no cached burden forecast for ${fullName}.`, diff --git a/src/services/burden-forecast.ts b/src/services/burden-forecast.ts index c4f850b3cf..4ba32a70ce 100644 --- a/src/services/burden-forecast.ts +++ b/src/services/burden-forecast.ts @@ -7,7 +7,9 @@ export type BurdenForecastFreshness = "fresh" | "stale"; export type BurdenForecastResponse = { status: "ready"; - source: "snapshot" | "computed"; + // Cache-only: the request-time compute path was removed in #906 (moved to the background + // `buildBurdenForecasts` job), so a response is always served from a stored snapshot (#8019). + source: "snapshot"; repoFullName: string; generatedAt: string; ageSeconds: number; @@ -15,7 +17,12 @@ export type BurdenForecastResponse = { report: BurdenForecast; }; -export async function loadOrComputeBurdenForecastResponse(env: Env, fullName: string): Promise { +/** + * Load the stored burden-forecast snapshot for a repo, or null when none is cached. This is cache-only + * (#8019): the inline compute fallback was removed in #906 and now runs as the background + * `buildBurdenForecasts` job (`src/queue/processors.ts`), which is what populates the snapshot read here. + */ +export async function loadCachedBurdenForecastResponse(env: Env, fullName: string): Promise { const repo = await getRepository(env, fullName); if (!repo) return null; diff --git a/test/unit/burden-forecast.test.ts b/test/unit/burden-forecast.test.ts index 54b60fbed8..4f7b7ffc38 100644 --- a/test/unit/burden-forecast.test.ts +++ b/test/unit/burden-forecast.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { getBurdenForecast, upsertBurdenForecast, upsertRepositoryFromGitHub } from "../../src/db/repositories"; -import { BURDEN_FORECAST_MAX_AGE_MS, loadOrComputeBurdenForecastResponse } from "../../src/services/burden-forecast"; +import { BURDEN_FORECAST_MAX_AGE_MS, loadCachedBurdenForecastResponse } from "../../src/services/burden-forecast"; import { buildBurdenForecast, buildCollisionReport } from "../../src/signals/engine"; import type { IssueRecord, JsonValue, PullRequestRecord, RepositoryRecord } from "../../src/types"; import { createTestEnv } from "../helpers/d1"; @@ -57,10 +57,10 @@ describe("burden forecast builder", () => { }); }); -describe("loadOrComputeBurdenForecastResponse", () => { +describe("loadCachedBurdenForecastResponse", () => { it("returns null when the repo is unknown", async () => { const env = createTestEnv(); - const response = await loadOrComputeBurdenForecastResponse(env, "ghost/missing"); + const response = await loadCachedBurdenForecastResponse(env, "ghost/missing"); expect(response).toBeNull(); }); @@ -76,7 +76,7 @@ describe("loadOrComputeBurdenForecastResponse", () => { generatedAt: new Date(Date.now() - 1000).toISOString(), }); - const response = await loadOrComputeBurdenForecastResponse(env, "ghost/private-repo"); + const response = await loadCachedBurdenForecastResponse(env, "ghost/private-repo"); expect(response).toBeNull(); }); @@ -89,7 +89,7 @@ describe("loadOrComputeBurdenForecastResponse", () => { payload: { repoFullName: "owner/fresh", level: "low", summary: "fresh fixture" } as unknown as Record, generatedAt: new Date(Date.now() - 60_000).toISOString(), }); - const response = await loadOrComputeBurdenForecastResponse(env, "owner/fresh"); + const response = await loadCachedBurdenForecastResponse(env, "owner/fresh"); expect(response).toMatchObject({ status: "ready", source: "snapshot", @@ -110,7 +110,7 @@ describe("loadOrComputeBurdenForecastResponse", () => { payload: { repoFullName: "owner/old", level: "high", summary: "stale fixture" } as unknown as Record, generatedAt, }); - const response = await loadOrComputeBurdenForecastResponse(env, "owner/old"); + const response = await loadCachedBurdenForecastResponse(env, "owner/old"); expect(response).toMatchObject({ status: "ready", source: "snapshot", @@ -135,7 +135,7 @@ describe("loadOrComputeBurdenForecastResponse", () => { generatedAt: "not-a-date", }); - const response = await loadOrComputeBurdenForecastResponse(env, "owner/malformed-time"); + const response = await loadCachedBurdenForecastResponse(env, "owner/malformed-time"); expect(response).toMatchObject({ status: "ready", @@ -150,7 +150,7 @@ describe("loadOrComputeBurdenForecastResponse", () => { const env = createTestEnv(); await upsertRepositoryFromGitHub(env, { name: "uncached", full_name: "owner/uncached", private: false, owner: { login: "owner" }, default_branch: "main" }); - const response = await loadOrComputeBurdenForecastResponse(env, "owner/uncached"); + const response = await loadCachedBurdenForecastResponse(env, "owner/uncached"); expect(response).toBeNull(); }); @@ -169,7 +169,7 @@ describe("loadOrComputeBurdenForecastResponse", () => { vi.spyOn(repositoriesModule, "listOpenPullRequests"), vi.spyOn(repositoriesModule, "listRecentMergedPullRequests"), ]; - await loadOrComputeBurdenForecastResponse(env, "owner/perf"); + await loadCachedBurdenForecastResponse(env, "owner/perf"); for (const spy of spies) { expect(spy).not.toHaveBeenCalled(); spy.mockRestore(); @@ -186,7 +186,7 @@ describe("loadOrComputeBurdenForecastResponse", () => { vi.spyOn(repositoriesModule, "listRecentMergedPullRequests"), ]; - const response = await loadOrComputeBurdenForecastResponse(env, "owner/computed-perf"); + const response = await loadCachedBurdenForecastResponse(env, "owner/computed-perf"); expect(response).toBeNull(); for (const spy of spies) {