Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}),
Expand Down
4 changes: 2 additions & 2 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -3455,7 +3455,7 @@ export class LoopoverMcp {
private async getBurdenForecast(input: { owner: string; repo: string }): Promise<ToolPayload> {
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}.`,
Expand Down
11 changes: 9 additions & 2 deletions src/services/burden-forecast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@ 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;
freshness: BurdenForecastFreshness;
report: BurdenForecast;
};

export async function loadOrComputeBurdenForecastResponse(env: Env, fullName: string): Promise<BurdenForecastResponse | null> {
/**
* 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<BurdenForecastResponse | null> {
const repo = await getRepository(env, fullName);
if (!repo) return null;

Expand Down
20 changes: 10 additions & 10 deletions test/unit/burden-forecast.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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();
});

Expand All @@ -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();
});
Expand All @@ -89,7 +89,7 @@ describe("loadOrComputeBurdenForecastResponse", () => {
payload: { repoFullName: "owner/fresh", level: "low", summary: "fresh fixture" } as unknown as Record<string, JsonValue>,
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",
Expand All @@ -110,7 +110,7 @@ describe("loadOrComputeBurdenForecastResponse", () => {
payload: { repoFullName: "owner/old", level: "high", summary: "stale fixture" } as unknown as Record<string, JsonValue>,
generatedAt,
});
const response = await loadOrComputeBurdenForecastResponse(env, "owner/old");
const response = await loadCachedBurdenForecastResponse(env, "owner/old");
expect(response).toMatchObject({
status: "ready",
source: "snapshot",
Expand All @@ -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",
Expand All @@ -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();
});
Expand All @@ -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();
Expand All @@ -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) {
Expand Down