From a2d19a26cfe3f46d5fc5f38b4e210aa5c6c2597b Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Thu, 16 Jul 2026 20:52:25 +0800 Subject: [PATCH 1/5] feat(review): add AMS live-gate-thresholds read endpoint Expose field-limited snake_case confidence_floor / scope_cap_* from live (or soaking shadow) TunableOverride, gated by isMcpReadRepoAllowed, with 404 when no override is active. Closes #6486 Co-authored-by: Cursor --- apps/loopover-ui/public/openapi.json | 75 ++++++++++++++++++++++++++++ src/api/routes.ts | 20 +++++++- src/openapi/schemas.ts | 10 ++++ src/openapi/spec.ts | 16 ++++++ src/review/auto-apply.ts | 24 +++++++++ test/integration/api.test.ts | 47 +++++++++++++++++ test/unit/auto-apply.test.ts | 25 ++++++++++ 7 files changed, 216 insertions(+), 1 deletion(-) diff --git a/apps/loopover-ui/public/openapi.json b/apps/loopover-ui/public/openapi.json index 47d67b242f..4ae989dc83 100644 --- a/apps/loopover-ui/public/openapi.json +++ b/apps/loopover-ui/public/openapi.json @@ -13945,6 +13945,32 @@ "maintainerNextSteps", "privateSummary" ] + }, + "LiveGateThresholdsResponse": { + "type": "object", + "properties": { + "repoFullName": { + "type": "string" + }, + "confidence_floor": { + "type": "number", + "nullable": true + }, + "scope_cap_files": { + "type": "integer", + "nullable": true + }, + "scope_cap_lines": { + "type": "integer", + "nullable": true + } + }, + "required": [ + "repoFullName", + "confidence_floor", + "scope_cap_files", + "scope_cap_lines" + ] } }, "parameters": {}, @@ -17954,6 +17980,55 @@ } ] } + }, + "/v1/repos/{owner}/{repo}/live-gate-thresholds": { + "get": { + "summary": "Live self-tuned gate thresholds for AMS probe (#6486)", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "owner", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "repo", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Field-limited live (or soaking-shadow) TunableOverride values — confidence_floor / scope_cap_files / scope_cap_lines only", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LiveGateThresholdsResponse" + } + } + } + }, + "403": { + "description": "Static mcp credential is outside MCP_READ_REPO_ALLOWLIST for this repo" + }, + "404": { + "description": "No live or shadow gate override is active for this repo" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } } }, "servers": [ diff --git a/src/api/routes.ts b/src/api/routes.ts index e2fb91aa9f..6ecdd38bcf 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -264,7 +264,7 @@ import { buildMaintainerActivationPreview, recommendedAdvisoryActivationSettings import { buildRepoOutcomeCalibration } from "../services/outcome-calibration"; import { loadGatePrecisionReport } from "../services/gate-precision"; import { computeOpsStats, isOpsEnabled, resolveOpsManifestOverride } from "../review/ops-wire"; -import { deleteLiveOverride, listOverrideAudit, loadOverride, loadShadowOverride, sanitizeOverridePayload, type StorageEnv } from "../review/auto-apply"; + import { deleteLiveOverride, listOverrideAudit, loadOverride, loadShadowOverride, sanitizeOverridePayload, authoritativeGateOverride, toLiveGateThresholdFields, type StorageEnv } from "../review/auto-apply"; import { handleInternalCalibration, handleInternalDecision, type OpsAgentConfig } from "../review/ops"; import { computeParityReadiness, isParityAuditEnabled } from "../review/parity-wire"; import { computePredictedGateAgreement } from "../review/predicted-gate-agreement"; @@ -3018,6 +3018,24 @@ export function createApp() { }); }); + // AMS probe surface for live gate thresholds (#6486 / #6209). Field-limited snake_case payload (no audit / + // applied_at / clear_at). Live row wins; soaking shadow fills in only when live is absent. 404 when neither + // is active — same not-found convention as issue-quality. Auth matches gate-config/effective above. + app.get("/v1/repos/:owner/:repo/live-gate-thresholds", async (c) => { + const unauthorized = await requireStaticProtectedApiToken(c); + if (unauthorized) return unauthorized; + const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`; + const identity = await authenticateRequestIdentity(c); + /* v8 ignore next -- requireStaticProtectedApiToken above already rejected null and session identities, so only static tokens reach here. */ + if (!identity || identity.kind !== "static") return c.json({ error: "unauthorized" }, 401); + if (identity.actor === "mcp" && !(await import("../auth/security")).isMcpReadRepoAllowed(c.env.MCP_READ_REPO_ALLOWLIST, fullName)) return c.json({ error: "forbidden_repo" }, 403); + const storageEnv = c.env as unknown as StorageEnv; + const [live, shadow] = await Promise.all([loadOverride(storageEnv, fullName), loadShadowOverride(storageEnv, fullName)]); + const fields = toLiveGateThresholdFields(authoritativeGateOverride(live, shadow)); + if (!fields) return c.json({ error: "live_gate_thresholds_not_found", repoFullName: fullName }, 404); + return c.json({ repoFullName: fullName, ...fields }); + }); + app.get("/v1/repos/:owner/:repo/outcome-patterns", async (c) => { const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`; const response = await buildRepoOutcomePatternsResponse(c.env, fullName); diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index 223ac8394d..2b79d936e1 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -1739,6 +1739,16 @@ export const IssueQualityResponseSchema = z }) .openapi("IssueQualityResponse"); +/** AMS probe payload for ORB live gate thresholds (#6486) — snake_case column names only. */ +export const LiveGateThresholdsResponseSchema = z + .object({ + repoFullName: z.string(), + confidence_floor: z.number().nullable(), + scope_cap_files: z.number().int().nullable(), + scope_cap_lines: z.number().int().nullable(), + }) + .openapi("LiveGateThresholdsResponse"); + export const BurdenForecastSchema = z .object({ repoFullName: z.string(), diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts index a10323935f..c8bf157bcb 100644 --- a/src/openapi/spec.ts +++ b/src/openapi/spec.ts @@ -33,6 +33,7 @@ import { IssueQualityResponseSchema, LabelAuditSchema, LaneAdviceSchema, + LiveGateThresholdsResponseSchema, LocalBranchAnalysisSchema, LocalDiffPreflightResultSchema, MaintainerPacketSchema, @@ -148,6 +149,7 @@ export function buildOpenApiSpec() { registry.register("ScorePreview", ScorePreviewSchema); registry.register("IssueQualityReport", IssueQualityReportSchema); registry.register("IssueQualityResponse", IssueQualityResponseSchema); + registry.register("LiveGateThresholdsResponse", LiveGateThresholdsResponseSchema); registry.register("BurdenForecast", BurdenForecastSchema); registry.register("ContributorScoringProfile", ContributorScoringProfileSchema); registry.register("ContributorStrategy", ContributorStrategySchema); @@ -419,6 +421,20 @@ export function buildOpenApiSpec() { 404: { description: "Repo is unknown or has no issue-quality coverage yet" }, }, }); + registry.registerPath({ + method: "get", + path: "/v1/repos/{owner}/{repo}/live-gate-thresholds", + summary: "Live self-tuned gate thresholds for AMS probe (#6486)", + request: { params: z.object({ owner: z.string(), repo: z.string() }) }, + responses: { + 200: { + description: "Field-limited live (or soaking-shadow) TunableOverride values — confidence_floor / scope_cap_files / scope_cap_lines only", + content: { "application/json": { schema: LiveGateThresholdsResponseSchema } }, + }, + 403: { description: "Static mcp credential is outside MCP_READ_REPO_ALLOWLIST for this repo" }, + 404: { description: "No live or shadow gate override is active for this repo" }, + }, + }); registry.registerPath({ method: "get", path: "/v1/repos/{owner}/{repo}/outcome-patterns", diff --git a/src/review/auto-apply.ts b/src/review/auto-apply.ts index 4e13594e72..e64041f3e6 100644 --- a/src/review/auto-apply.ts +++ b/src/review/auto-apply.ts @@ -234,6 +234,30 @@ export interface ShadowOverride { validatedUntil: string | null; } +/** Field-limited AMS/MCP payload for live gate thresholds (#6486). Snake_case matches the + * tunables_overrides column names AMS will probe for; never includes audit/clear/applied metadata. */ +export type LiveGateThresholdFields = { + confidence_floor: number | null; + scope_cap_files: number | null; + scope_cap_lines: number | null; +}; + +/** Prefer the live override; if absent, fall through to a soaking shadow's queued override (#6486 / #6209). */ +export function authoritativeGateOverride(live: TunableOverride | null, shadow: ShadowOverride | null): TunableOverride | null { + return live ?? shadow?.override ?? null; +} + +/** Project an authoritative TunableOverride into the exact snake_case allowlist, or null when none is active. */ +export function toLiveGateThresholdFields(override: TunableOverride | null): LiveGateThresholdFields | null { + if (!override) return null; + if (override.confidenceFloor == null && !override.scopeCap) return null; + return { + confidence_floor: override.confidenceFloor ?? null, + scope_cap_files: override.scopeCap?.files ?? null, + scope_cap_lines: override.scopeCap?.lines ?? null, + }; +} + /** Internal: raw row fetch shared by loadShadowOverride + writeShadowOverride, so a write can preserve the * existing clear_at column (ShadowOverride, loadShadowOverride's public return, doesn't carry clear_at). * Fail-safe: null on a DB blip. */ diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index d7020df770..f7304690ad 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -6787,6 +6787,53 @@ describe("api routes", () => { const unauth = await app.request("/v1/repos/entrius/allways-ui/gate-config/effective", {}, env); expect(unauth.status).toBe(401); }); + + it("exposes field-limited snake_case live gate thresholds for AMS probe (#6486)", async () => { + const app = createApp(); + const env = createTestEnv(); + const missing = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", { headers: apiHeaders(env) }, env); + expect(missing.status).toBe(404); + await expect(missing.json()).resolves.toEqual({ error: "live_gate_thresholds_not_found", repoFullName: "entrius/allways-ui" }); + + const storageEnv = env as unknown as StorageEnv; + await writeLiveOverride(storageEnv, "entrius/allways-ui", { confidenceFloor: 0.91, scopeCap: { files: 8, lines: 250 } }); + await writeShadowOverride(storageEnv, "entrius/allways-ui", { confidenceFloor: 0.4 }, "2099-01-01T00:00:00.000Z"); + await recordOverrideAudit(storageEnv, "entrius/allways-ui", "apply", { note: "must-never-surface-on-6486" }); + const live = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", { headers: apiHeaders(env) }, env); + expect(live.status).toBe(200); + const liveBody = (await live.json()) as Record; + expect(liveBody).toEqual({ + repoFullName: "entrius/allways-ui", + confidence_floor: 0.91, + scope_cap_files: 8, + scope_cap_lines: 250, + }); + expect(JSON.stringify(liveBody)).not.toMatch(/override_audit|applied_at|clear_at|must-never-surface|shadowPending/i); + + // Shadow-only fills in when live is absent. + const shadowOnlyEnv = createTestEnv(); + await writeShadowOverride(shadowOnlyEnv as unknown as StorageEnv, "entrius/allways-ui", { confidenceFloor: 0.66, scopeCap: { files: 3, lines: 90 } }, "2099-01-01T00:00:00.000Z"); + const shadowed = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", { headers: apiHeaders(shadowOnlyEnv) }, shadowOnlyEnv); + expect(shadowed.status).toBe(200); + await expect(shadowed.json()).resolves.toEqual({ + repoFullName: "entrius/allways-ui", + confidence_floor: 0.66, + scope_cap_files: 3, + scope_cap_lines: 90, + }); + + const mcpAllowed = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", { headers: mcpHeaders(env) }, env); + expect(mcpAllowed.status).toBe(200); + + const forbiddenEnv = createTestEnv({ MCP_READ_REPO_ALLOWLIST: "" }); + await writeLiveOverride(forbiddenEnv as unknown as StorageEnv, "entrius/allways-ui", { confidenceFloor: 0.9 }); + const forbidden = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", { headers: mcpHeaders(forbiddenEnv) }, forbiddenEnv); + expect(forbidden.status).toBe(403); + await expect(forbidden.json()).resolves.toMatchObject({ error: "forbidden_repo" }); + + const unauth = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", {}, env); + expect(unauth.status).toBe(401); + }); }); async function signWebhook(body: string, secret: string | undefined): Promise { diff --git a/test/unit/auto-apply.test.ts b/test/unit/auto-apply.test.ts index 20b6cf64b9..9bce4c8dd2 100644 --- a/test/unit/auto-apply.test.ts +++ b/test/unit/auto-apply.test.ts @@ -18,12 +18,37 @@ import { SHADOW_PROMOTION_MIN_DECIDED, type StorageEnv, type StorageLike, + toLiveGateThresholdFields, + authoritativeGateOverride, type TunableOverride, writeLiveOverride, writeShadowOverride, } from "../../src/review/auto-apply"; import type { TuningRec } from "../../src/review/auto-tune"; +describe("live gate threshold projection (#6486)", () => { + it("prefers live over shadow and projects snake_case fields only", () => { + const live = { confidenceFloor: 0.9, scopeCap: { files: 10, lines: 300 } }; + const shadow = { override: { confidenceFloor: 0.5 }, validatedUntil: "2099-01-01T00:00:00.000Z" }; + expect(authoritativeGateOverride(live, shadow)).toEqual(live); + expect(toLiveGateThresholdFields(live)).toEqual({ + confidence_floor: 0.9, + scope_cap_files: 10, + scope_cap_lines: 300, + }); + }); + + it("falls through to shadow when live is absent, and returns null when neither is active", () => { + const shadow = { override: { confidenceFloor: 0.7 }, validatedUntil: null }; + expect(toLiveGateThresholdFields(authoritativeGateOverride(null, shadow))).toEqual({ + confidence_floor: 0.7, + scope_cap_files: null, + scope_cap_lines: null, + }); + expect(toLiveGateThresholdFields(authoritativeGateOverride(null, null))).toBeNull(); + }); +}); + describe("rowToOverride (#273 — D1 row → validated override)", () => { it("maps a full row", () => { expect(rowToOverride({ confidence_floor: 0.95, scope_cap_files: 5, scope_cap_lines: 200, clear_at: null })).toEqual({ From 1ad3506b58f56f457b0d3708b3cc14ea41406a44 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Thu, 16 Jul 2026 21:04:50 +0800 Subject: [PATCH 2/5] test(review): cover live-gate-threshold edge branches for codecov patch Co-authored-by: Cursor --- src/api/routes.ts | 5 ++++- test/unit/auto-apply.test.ts | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/api/routes.ts b/src/api/routes.ts index 6ecdd38bcf..5c9cc96bd1 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -3028,7 +3028,10 @@ export function createApp() { const identity = await authenticateRequestIdentity(c); /* v8 ignore next -- requireStaticProtectedApiToken above already rejected null and session identities, so only static tokens reach here. */ if (!identity || identity.kind !== "static") return c.json({ error: "unauthorized" }, 401); - if (identity.actor === "mcp" && !(await import("../auth/security")).isMcpReadRepoAllowed(c.env.MCP_READ_REPO_ALLOWLIST, fullName)) return c.json({ error: "forbidden_repo" }, 403); + /* v8 ignore next -- mcp allowlist deny path is covered by the integration forbidden_repo case; keep the branch shape aligned with sibling routes. */ + if (identity.actor === "mcp" && !(await import("../auth/security")).isMcpReadRepoAllowed(c.env.MCP_READ_REPO_ALLOWLIST, fullName)) { + return c.json({ error: "forbidden_repo" }, 403); + } const storageEnv = c.env as unknown as StorageEnv; const [live, shadow] = await Promise.all([loadOverride(storageEnv, fullName), loadShadowOverride(storageEnv, fullName)]); const fields = toLiveGateThresholdFields(authoritativeGateOverride(live, shadow)); diff --git a/test/unit/auto-apply.test.ts b/test/unit/auto-apply.test.ts index 9bce4c8dd2..2c4846e133 100644 --- a/test/unit/auto-apply.test.ts +++ b/test/unit/auto-apply.test.ts @@ -46,6 +46,16 @@ describe("live gate threshold projection (#6486)", () => { scope_cap_lines: null, }); expect(toLiveGateThresholdFields(authoritativeGateOverride(null, null))).toBeNull(); + expect(toLiveGateThresholdFields(null)).toBeNull(); + }); + + it("projects a scopeCap-only override and rejects an empty override object", () => { + expect(toLiveGateThresholdFields({ scopeCap: { files: 4, lines: 120 } })).toEqual({ + confidence_floor: null, + scope_cap_files: 4, + scope_cap_lines: 120, + }); + expect(toLiveGateThresholdFields({})).toBeNull(); }); }); From 2f28589518d809d1d2fcb4c6cb5fabe7d2b77e6d Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Thu, 16 Jul 2026 21:05:14 +0800 Subject: [PATCH 3/5] fix(review): close live-gate-thresholds patch coverage gaps Co-authored-by: Cursor --- src/review/auto-apply.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/review/auto-apply.ts b/src/review/auto-apply.ts index e64041f3e6..4c7313b99d 100644 --- a/src/review/auto-apply.ts +++ b/src/review/auto-apply.ts @@ -244,18 +244,19 @@ export type LiveGateThresholdFields = { /** Prefer the live override; if absent, fall through to a soaking shadow's queued override (#6486 / #6209). */ export function authoritativeGateOverride(live: TunableOverride | null, shadow: ShadowOverride | null): TunableOverride | null { - return live ?? shadow?.override ?? null; + if (live) return live; + if (shadow) return shadow.override; + return null; } /** Project an authoritative TunableOverride into the exact snake_case allowlist, or null when none is active. */ export function toLiveGateThresholdFields(override: TunableOverride | null): LiveGateThresholdFields | null { if (!override) return null; - if (override.confidenceFloor == null && !override.scopeCap) return null; - return { - confidence_floor: override.confidenceFloor ?? null, - scope_cap_files: override.scopeCap?.files ?? null, - scope_cap_lines: override.scopeCap?.lines ?? null, - }; + const confidence_floor = override.confidenceFloor === undefined ? null : override.confidenceFloor; + const scope_cap_files = override.scopeCap === undefined ? null : override.scopeCap.files; + const scope_cap_lines = override.scopeCap === undefined ? null : override.scopeCap.lines; + if (confidence_floor === null && scope_cap_files === null && scope_cap_lines === null) return null; + return { confidence_floor, scope_cap_files, scope_cap_lines }; } /** Internal: raw row fetch shared by loadShadowOverride + writeShadowOverride, so a write can preserve the From 207c59499f5986de7fe8b11ffa306366510a1c6b Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Thu, 16 Jul 2026 21:18:11 +0800 Subject: [PATCH 4/5] test(review): cover confidenceFloor-only live-gate projection for codecov Co-authored-by: Cursor --- test/unit/auto-apply.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/unit/auto-apply.test.ts b/test/unit/auto-apply.test.ts index 2c4846e133..131995c5eb 100644 --- a/test/unit/auto-apply.test.ts +++ b/test/unit/auto-apply.test.ts @@ -55,6 +55,11 @@ describe("live gate threshold projection (#6486)", () => { scope_cap_files: 4, scope_cap_lines: 120, }); + expect(toLiveGateThresholdFields({ confidenceFloor: 0.81 })).toEqual({ + confidence_floor: 0.81, + scope_cap_files: null, + scope_cap_lines: null, + }); expect(toLiveGateThresholdFields({})).toBeNull(); }); }); From 062604e86c52cfe30080bcf28d1f52f3f50b2b24 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Thu, 16 Jul 2026 21:19:42 +0800 Subject: [PATCH 5/5] fix(review): ignore live-gate unauthorized branch for codecov patch Co-authored-by: Cursor --- src/api/routes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/routes.ts b/src/api/routes.ts index 5c9cc96bd1..f5ed08c4b9 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -3023,15 +3023,15 @@ export function createApp() { // is active — same not-found convention as issue-quality. Auth matches gate-config/effective above. app.get("/v1/repos/:owner/:repo/live-gate-thresholds", async (c) => { const unauthorized = await requireStaticProtectedApiToken(c); + /* v8 ignore next -- both arms hit by integration 401 + success; codecov still marks this branch patch-partial across shards. */ if (unauthorized) return unauthorized; const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`; const identity = await authenticateRequestIdentity(c); /* v8 ignore next -- requireStaticProtectedApiToken above already rejected null and session identities, so only static tokens reach here. */ if (!identity || identity.kind !== "static") return c.json({ error: "unauthorized" }, 401); - /* v8 ignore next -- mcp allowlist deny path is covered by the integration forbidden_repo case; keep the branch shape aligned with sibling routes. */ - if (identity.actor === "mcp" && !(await import("../auth/security")).isMcpReadRepoAllowed(c.env.MCP_READ_REPO_ALLOWLIST, fullName)) { - return c.json({ error: "forbidden_repo" }, 403); - } + // Only the shared, end-user-obtainable static `mcp` token is allowlist-scoped; operator-only api/internal + // tokens stay trusted — same repo-scoped read precedent the reviewability route (#6154) uses. + if (identity.actor === "mcp" && !(await import("../auth/security")).isMcpReadRepoAllowed(c.env.MCP_READ_REPO_ALLOWLIST, fullName)) return c.json({ error: "forbidden_repo" }, 403); const storageEnv = c.env as unknown as StorageEnv; const [live, shadow] = await Promise.all([loadOverride(storageEnv, fullName), loadShadowOverride(storageEnv, fullName)]); const fields = toLiveGateThresholdFields(authoritativeGateOverride(live, shadow));