Skip to content

Commit ba93111

Browse files
authored
feat(review): backtest-gated autonomous loosening of the satisfaction confidence floor (#8121 narrow start) (#8163)
auto-tune.ts's OverridePayload states the historical rule: a loosening recommendation never carries a payload, because autonomous loosening is the regression risk the loop exists to avoid. The #8082 backtest primitives are exactly the missing risk measurement -- this ships the epic's approved narrow start for its named first candidate, LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR. evaluateSatisfactionFloorLoosening (pure) proposes the SMALLEST candidate step below the current floor that scores strictly improved on the visible split AND non-regressed on the deterministic held-out split (#8087's fixed-seed split, so repeated runs can never fish for a lucky partition), never below a hard 0.3 safety minimum, never on a small corpus. The run layer applies a cleared proposal by writing a system_flags override (the same operational-flag table the circuit breakers use) plus a calibration.satisfaction_floor_loosened audit event, with independent direction/bound re-checks at the write path. The live floor resolves inside runLoopOverLinkedIssueSatisfaction; the pure parse takes the floor as a defaulted parameter, byte-identical when absent. Everything is gated on SATISFACTION_FLOOR_AUTOTUNE_ENABLED (wrangler var, false by default): the override read, the apply path, and the manual trigger route POST /v1/internal/calibration/loosen-satisfaction-floor all 404/no-op until the operator opts in, and flipping the flag off instantly restores the shipped floor. 100% line+branch coverage on both new modules; floor threading covered in the pure and run suites. Advances #8121.
1 parent 9c303a6 commit ba93111

14 files changed

Lines changed: 634 additions & 7 deletions

src/api/routes.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ import { isRagEnabled } from "../review/rag-wire";
320320
import { getPublicStats, isPublicStatsEnabled, resolvePublicStatsManifestOverride } from "../review/public-stats";
321321
import { loadPublicAccuracyTrend } from "../services/public-accuracy-trend";
322322
import { loadCalibrationTrend } from "../services/rule-calibration-trend";
323+
import { isSatisfactionFloorAutotuneEnabled, runSatisfactionFloorLoosening } from "../services/satisfaction-floor-loosening-run";
323324
import { loadPublicReuseRateTrend } from "../services/public-reuse-rate-trend";
324325
import { loadPublicReviewVolumeTrend } from "../services/public-review-volume-trend";
325326
import { buildMaintainerQualityDashboard, isMaintainerQualityDataStale } from "../services/maintainer-quality-dashboard";
@@ -4814,6 +4815,17 @@ export function createApp() {
48144815
// Aggregate counts and rule ids only — no PR content, no raw context.
48154816
app.get("/v1/internal/calibration-trend", async (c) => c.json(await loadCalibrationTrend(c.env)));
48164817

4818+
// #8121 (approved narrow start): manually trigger one backtest-gated loosening evaluation of the
4819+
// linked-issue satisfaction confidence floor. 404 when the autotune flag is off (the endpoint doesn't
4820+
// exist on a deploy that hasn't opted in, mirroring the rag-index route's flag-gate). Bearer-gated by the
4821+
// /v1/internal/* middleware (INTERNAL_JOB_TOKEN). Applying is idempotent per candidate step: repeat calls
4822+
// re-evaluate from the CURRENT (possibly already-loosened) floor and step at most one candidate at a time.
4823+
app.post("/v1/internal/calibration/loosen-satisfaction-floor", async (c) => {
4824+
if (!isSatisfactionFloorAutotuneEnabled(c.env)) return c.json({ error: "not_found" }, 404);
4825+
const result = await runSatisfactionFloorLoosening(c.env);
4826+
return c.json(result);
4827+
});
4828+
48174829
app.post("/v1/internal/jobs/refresh-registry", async (c) => {
48184830
const message: JobMessage = { type: "refresh-registry", requestedBy: "api" };
48194831
await c.env.JOBS.send(message);

src/env.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ declare global {
227227
* fetch (cloud, or a self-host without the dir, is byte-identical to before). */
228228
LOOPOVER_REPO_CONFIG_DIR?: string;
229229
LOOPOVER_AUTO_FILE_DRIFT_ISSUES?: string;
230+
// #8121: backtest-gated satisfaction-floor autotune go-live switch (wrangler var, "false" by default).
231+
SATISFACTION_FLOOR_AUTOTUNE_ENABLED?: string;
230232
LOOPOVER_DRIFT_ISSUE_REPO?: string;
231233
LOOPOVER_DRIFT_ISSUE_TOKEN?: string;
232234
/** Comma-separated GitHub logins assigned to filed upstream-drift issues (default: the loopover

src/services/linked-issue-satisfaction-run.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// bounded, public-safe {status, rationale} (or null). The caller (src/queue/processors.ts) decides.
1414
import type { LinkedIssueSatisfactionResult } from "./linked-issue-satisfaction";
1515
import { SATISFACTION_SYSTEM_PROMPT, buildLinkedIssueSatisfactionPrompt, buildLinkedIssueSatisfactionResult } from "./linked-issue-satisfaction";
16+
import { getSatisfactionFloorOverride } from "./satisfaction-floor-loosening-run";
1617
import { countByokAiEventsForRepoSince, recordAiUsageEvent, sumAiEstimatedNeuronsSince } from "../db/repositories";
1718
import {
1819
type AiReviewActualUsage,
@@ -43,6 +44,10 @@ export type LinkedIssueSatisfactionRunInput = {
4344
/** Optional BYOK: when present, the maintainer's frontier model writes the assessment (billed to their
4445
* account, counted against the shared per-repo/day BYOK cap) instead of the free/default reviewer. */
4546
providerKey?: AiReviewProviderKey | null | undefined;
47+
/** #8121: optional explicit confidence floor. Absent ⇒ the run resolves the live backtest-gated override
48+
* itself (getSatisfactionFloorOverride; null when the autotune flag is off), falling back to the pure
49+
* module's shipped constant — so every caller gets the loosened floor with zero threading. */
50+
confidenceFloor?: number | undefined;
4651
};
4752

4853
export type LinkedIssueSatisfactionRunResult =
@@ -72,6 +77,7 @@ async function runWorkersSatisfactionOpinion(
7277
system: string,
7378
user: string,
7479
maxTokens: number,
80+
confidenceFloor?: number,
7581
): Promise<WorkersSatisfactionOpinionResult> {
7682
const ai = env.AI as unknown as AiRunner | undefined;
7783
if (!ai || typeof ai.run !== "function") return { result: null };
@@ -86,7 +92,7 @@ async function runWorkersSatisfactionOpinion(
8692
extra,
8793
);
8894
const text = coerceAiText(raw);
89-
const result = buildLinkedIssueSatisfactionResult(issueText, text);
95+
const result = buildLinkedIssueSatisfactionResult(issueText, text, confidenceFloor);
9096
if (result) return { result, usage: coerceAiUsage(raw), rawText: text };
9197
} catch (error) {
9298
if (isRateLimitError(error)) break;
@@ -109,6 +115,11 @@ export async function runLoopOverLinkedIssueSatisfaction(env: Env, input: Linked
109115
// nothing to assess, so short-circuit before spending any budget or making a model call.
110116
if (!(input.issueText ?? "").trim()) return { status: "ok", result: null, estimatedNeurons: 0 };
111117

118+
// #8121: resolve the live backtest-gated floor override HERE (single resolution point for every caller)
119+
// unless the caller supplied an explicit floor. Null (flag off / no valid override) keeps the pure
120+
// module's shipped constant via the parameter default -- byte-identical to pre-#8121 behavior.
121+
const confidenceFloor = input.confidenceFloor ?? (await getSatisfactionFloorOverride(env)) ?? undefined;
122+
112123
const maxTokens = clampNumber(Number(env.AI_MAX_OUTPUT_TOKENS || 256), 256, 1024);
113124
const user = buildLinkedIssueSatisfactionPrompt({
114125
issueText: input.issueText,
@@ -146,11 +157,11 @@ export async function runLoopOverLinkedIssueSatisfaction(env: Env, input: Linked
146157
let rawModelText: string | undefined;
147158
if (input.providerKey) {
148159
const { text, usage: byokUsage } = await callAiProvider(input.providerKey, SATISFACTION_SYSTEM_PROMPT, user, maxTokens);
149-
result = text ? buildLinkedIssueSatisfactionResult(input.issueText, text) : null;
160+
result = text ? buildLinkedIssueSatisfactionResult(input.issueText, text, confidenceFloor) : null;
150161
usage = byokUsage;
151162
rawModelText = text || undefined;
152163
} else {
153-
({ result, usage, rawText: rawModelText } = await runWorkersSatisfactionOpinion(env, input.issueText, SATISFACTION_SYSTEM_PROMPT, user, maxTokens));
164+
({ result, usage, rawText: rawModelText } = await runWorkersSatisfactionOpinion(env, input.issueText, SATISFACTION_SYSTEM_PROMPT, user, maxTokens, confidenceFloor));
154165
}
155166
await record(env, input, "ok", estimatedNeurons, result ? `advisory finding (${result.status})` : "no usable output", { status: result?.status ?? null, surfaced: Boolean(result), byok: Boolean(input.providerKey) }, usage);
156167
return { status: "ok", result, estimatedNeurons, ...(rawModelText ? { rawModelText } : {}) };

src/services/linked-issue-satisfaction.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ export function buildLinkedIssueSatisfactionPrompt(input: LinkedIssueSatisfactio
112112
/** Parse the model's raw JSON text response into a {@link LinkedIssueSatisfactionResult}, or null when the
113113
* output is unusable (no JSON object, invalid status, or the confidence floor rejects an "unaddressed" call).
114114
* PURE — never throws (a malformed blob that matches the brace regex but fails JSON.parse is caught). */
115-
export function parseLinkedIssueSatisfactionOpinion(text: string): LinkedIssueSatisfactionResult | null {
115+
export function parseLinkedIssueSatisfactionOpinion(
116+
text: string,
117+
// #8121: the floor is overridable ONLY downward and only via the backtest-gated loosening loop -- callers
118+
// without an override pass nothing and get the shipped constant, byte-identical to before.
119+
confidenceFloor: number = LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR,
120+
): LinkedIssueSatisfactionResult | null {
116121
const match = text
117122
.replace(/^```(?:json)?\s*/i, "")
118123
.replace(/```$/i, "")
@@ -129,7 +134,7 @@ export function parseLinkedIssueSatisfactionOpinion(text: string): LinkedIssueSa
129134
const confidence = parseConfidence(obj.confidence);
130135
// Fail-safe floor (#2172): a low-confidence "unaddressed" is never published as unaddressed — the caller
131136
// gets no finding at all rather than a shaky "you didn't fix this" call. addressed/partial are unaffected.
132-
if (obj.status === "unaddressed" && confidence < LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR) return null;
137+
if (obj.status === "unaddressed" && confidence < confidenceFloor) return null;
133138
if (!rationale) return null;
134139
return { status: obj.status, rationale, confidence };
135140
}
@@ -143,10 +148,11 @@ export function parseLinkedIssueSatisfactionOpinion(text: string): LinkedIssueSa
143148
export function buildLinkedIssueSatisfactionResult(
144149
issueText: string | null | undefined,
145150
modelResponseText: string,
151+
confidenceFloor: number = LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR,
146152
): LinkedIssueSatisfactionResult | null {
147153
if (!(issueText ?? "").trim()) return null;
148154
try {
149-
const opinion = parseLinkedIssueSatisfactionOpinion(modelResponseText);
155+
const opinion = parseLinkedIssueSatisfactionOpinion(modelResponseText, confidenceFloor);
150156
if (!opinion) return null;
151157
const safeRationale = toPublicSafe(opinion.rationale);
152158
if (!safeRationale) return null;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// IO orchestration for the backtest-gated satisfaction-floor loosening (#8121 narrow start) — the
2+
// "separate, I/O-touching slice" satisfaction-floor-loosening.ts leaves to its caller, mirroring
3+
// threshold-backtest-run.ts's identical split. Three responsibilities:
4+
// 1. read the live floor override (system_flags, migration 0054 — the same operational-flag table the
5+
// auto-tune circuit breakers use, so no new storage surface);
6+
// 2. evaluate a loosening against the rule's real recorded history (SignalStore → corpus → pure core);
7+
// 3. apply an approved proposal: write the override + a calibration audit event.
8+
//
9+
// The ENTIRE apply path is flag-gated on env.SATISFACTION_FLOOR_AUTOTUNE_ENABLED (wrangler var, unset/false
10+
// by default), so a deploy without the flag is behavior-identical — #8121's Boundaries demand no autonomous
11+
// config change without the explicit opt-in. Direction is enforced here AGAIN (proposed < current, ≥ hard
12+
// minimum) on top of the evaluator's own guarantee: the write path must be independently incapable of
13+
// tightening-disguised-as-loosening or of sailing past the safety minimum, whatever its input claims.
14+
import { buildBacktestCorpus } from "@loopover/engine";
15+
import { createSignalStore } from "../review/signal-tracking-wire";
16+
import { recordAuditEvent } from "../db/repositories";
17+
import {
18+
evaluateSatisfactionFloorLoosening,
19+
SATISFACTION_FLOOR_HARD_MINIMUM,
20+
SATISFACTION_FLOOR_RULE_ID,
21+
type SatisfactionFloorLooseningProposal,
22+
} from "./satisfaction-floor-loosening";
23+
import { LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR } from "./linked-issue-satisfaction";
24+
25+
export const SATISFACTION_FLOOR_OVERRIDE_FLAG_KEY = "satisfaction_floor_override";
26+
export const SATISFACTION_FLOOR_LOOSENING_EVENT_TYPE = "calibration.satisfaction_floor_loosened";
27+
const CORPUS_LOOKBACK_MS = 90 * 24 * 60 * 60 * 1000; // mirrors threshold-backtest-run's 90-day window
28+
29+
/** Truthy-string env flag, matching the repo's flag convention (mirrors outcomes-wire's flagTruthy). */
30+
export function isSatisfactionFloorAutotuneEnabled(env: Env): boolean {
31+
const value = (env.SATISFACTION_FLOOR_AUTOTUNE_ENABLED ?? "").trim().toLowerCase();
32+
return value === "1" || value === "true" || value === "on" || value === "yes";
33+
}
34+
35+
/**
36+
* Read the live floor override. Returns null (caller uses the shipped default) when: the autotune flag is
37+
* off (an operator turning the feature off instantly restores the shipped floor, no cleanup required), no
38+
* override row exists, or the stored value fails validation — an override may only ever sit BELOW the
39+
* shipped floor and AT/ABOVE the hard minimum, so a corrupted/hand-edited row can never tighten the floor
40+
* or loosen it past safety. Fail-safe null on any DB error (the shipped default is always the fallback).
41+
*/
42+
export async function getSatisfactionFloorOverride(env: Env): Promise<number | null> {
43+
if (!isSatisfactionFloorAutotuneEnabled(env)) return null;
44+
try {
45+
const row = await env.DB.prepare("SELECT value FROM system_flags WHERE key = ?")
46+
.bind(SATISFACTION_FLOOR_OVERRIDE_FLAG_KEY)
47+
.first<{ value: string }>();
48+
if (!row) return null;
49+
const parsed = Number(row.value);
50+
if (!Number.isFinite(parsed) || parsed >= LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR || parsed < SATISFACTION_FLOOR_HARD_MINIMUM) {
51+
return null;
52+
}
53+
return parsed;
54+
} catch {
55+
return null;
56+
}
57+
}
58+
59+
export type SatisfactionFloorLooseningRunResult =
60+
| { applied: false; reason: "flag_off" | "no_proposal" | "already_applied" }
61+
| { applied: true; proposal: SatisfactionFloorLooseningProposal };
62+
63+
/**
64+
* Evaluate and (when justified) apply a backtest-gated loosening of the satisfaction floor. The current
65+
* floor is the live override when one exists (so repeated runs evaluate from where the system actually is,
66+
* stepping at most one candidate per run, and can never oscillate upward). Persists the new override plus a
67+
* `calibration.satisfaction_floor_loosened` audit event carrying both split comparisons — the same
68+
* structured evidence trail every other calibration write in epic #8082 leaves. Audit write is best-effort;
69+
* the override write is NOT (an unrecorded floor change would be worse than no change, so a failed flag
70+
* write aborts by throwing to the caller — the internal route surfaces it as a 500).
71+
*/
72+
export async function runSatisfactionFloorLoosening(env: Env, nowMs: number = Date.now()): Promise<SatisfactionFloorLooseningRunResult> {
73+
if (!isSatisfactionFloorAutotuneEnabled(env)) return { applied: false, reason: "flag_off" };
74+
75+
const currentFloor = (await getSatisfactionFloorOverride(env)) ?? LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR;
76+
if (currentFloor <= SATISFACTION_FLOOR_HARD_MINIMUM) return { applied: false, reason: "already_applied" };
77+
78+
const { fired, overrides } = await createSignalStore(env).queryRuleHistory(SATISFACTION_FLOOR_RULE_ID, nowMs - CORPUS_LOOKBACK_MS);
79+
const cases = buildBacktestCorpus(SATISFACTION_FLOOR_RULE_ID, fired, overrides);
80+
const proposal = evaluateSatisfactionFloorLoosening(cases, currentFloor);
81+
if (!proposal) return { applied: false, reason: "no_proposal" };
82+
// Defense in depth: the write path independently refuses anything that isn't a strict, bounded loosening.
83+
if (proposal.proposedFloor >= currentFloor || proposal.proposedFloor < SATISFACTION_FLOOR_HARD_MINIMUM) {
84+
return { applied: false, reason: "no_proposal" };
85+
}
86+
87+
await env.DB.prepare(
88+
"INSERT INTO system_flags (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at",
89+
)
90+
.bind(SATISFACTION_FLOOR_OVERRIDE_FLAG_KEY, String(proposal.proposedFloor))
91+
.run();
92+
93+
await recordAuditEvent(env, {
94+
eventType: SATISFACTION_FLOOR_LOOSENING_EVENT_TYPE,
95+
actor: "loopover",
96+
targetKey: SATISFACTION_FLOOR_RULE_ID,
97+
outcome: "completed",
98+
detail: `satisfaction confidence floor loosened ${proposal.currentFloor} -> ${proposal.proposedFloor} (backtest-gated, visible improved + held-out non-regressed)`,
99+
metadata: { proposal },
100+
}).catch(() => undefined);
101+
102+
return { applied: true, proposal };
103+
}

0 commit comments

Comments
 (0)