diff --git a/src/api/routes.ts b/src/api/routes.ts index 74caa6eef3..a41cfd671c 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -320,6 +320,7 @@ import { isRagEnabled } from "../review/rag-wire"; import { getPublicStats, isPublicStatsEnabled, resolvePublicStatsManifestOverride } from "../review/public-stats"; import { loadPublicAccuracyTrend } from "../services/public-accuracy-trend"; import { loadCalibrationTrend } from "../services/rule-calibration-trend"; +import { isSatisfactionFloorAutotuneEnabled, runSatisfactionFloorLoosening } from "../services/satisfaction-floor-loosening-run"; import { loadPublicReuseRateTrend } from "../services/public-reuse-rate-trend"; import { loadPublicReviewVolumeTrend } from "../services/public-review-volume-trend"; import { buildMaintainerQualityDashboard, isMaintainerQualityDataStale } from "../services/maintainer-quality-dashboard"; @@ -4813,6 +4814,17 @@ export function createApp() { // Aggregate counts and rule ids only — no PR content, no raw context. app.get("/v1/internal/calibration-trend", async (c) => c.json(await loadCalibrationTrend(c.env))); + // #8121 (approved narrow start): manually trigger one backtest-gated loosening evaluation of the + // linked-issue satisfaction confidence floor. 404 when the autotune flag is off (the endpoint doesn't + // exist on a deploy that hasn't opted in, mirroring the rag-index route's flag-gate). Bearer-gated by the + // /v1/internal/* middleware (INTERNAL_JOB_TOKEN). Applying is idempotent per candidate step: repeat calls + // re-evaluate from the CURRENT (possibly already-loosened) floor and step at most one candidate at a time. + app.post("/v1/internal/calibration/loosen-satisfaction-floor", async (c) => { + if (!isSatisfactionFloorAutotuneEnabled(c.env)) return c.json({ error: "not_found" }, 404); + const result = await runSatisfactionFloorLoosening(c.env); + return c.json(result); + }); + app.post("/v1/internal/jobs/refresh-registry", async (c) => { const message: JobMessage = { type: "refresh-registry", requestedBy: "api" }; await c.env.JOBS.send(message); diff --git a/src/env.d.ts b/src/env.d.ts index a72c059f34..3d9ab7e811 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -227,6 +227,8 @@ declare global { * fetch (cloud, or a self-host without the dir, is byte-identical to before). */ LOOPOVER_REPO_CONFIG_DIR?: string; LOOPOVER_AUTO_FILE_DRIFT_ISSUES?: string; + // #8121: backtest-gated satisfaction-floor autotune go-live switch (wrangler var, "false" by default). + SATISFACTION_FLOOR_AUTOTUNE_ENABLED?: string; LOOPOVER_DRIFT_ISSUE_REPO?: string; LOOPOVER_DRIFT_ISSUE_TOKEN?: string; /** Comma-separated GitHub logins assigned to filed upstream-drift issues (default: the loopover diff --git a/src/services/linked-issue-satisfaction-run.ts b/src/services/linked-issue-satisfaction-run.ts index 375d82c58d..6e9ee53c66 100644 --- a/src/services/linked-issue-satisfaction-run.ts +++ b/src/services/linked-issue-satisfaction-run.ts @@ -13,6 +13,7 @@ // bounded, public-safe {status, rationale} (or null). The caller (src/queue/processors.ts) decides. import type { LinkedIssueSatisfactionResult } from "./linked-issue-satisfaction"; import { SATISFACTION_SYSTEM_PROMPT, buildLinkedIssueSatisfactionPrompt, buildLinkedIssueSatisfactionResult } from "./linked-issue-satisfaction"; +import { getSatisfactionFloorOverride } from "./satisfaction-floor-loosening-run"; import { countByokAiEventsForRepoSince, recordAiUsageEvent, sumAiEstimatedNeuronsSince } from "../db/repositories"; import { type AiReviewActualUsage, @@ -43,6 +44,10 @@ export type LinkedIssueSatisfactionRunInput = { /** Optional BYOK: when present, the maintainer's frontier model writes the assessment (billed to their * account, counted against the shared per-repo/day BYOK cap) instead of the free/default reviewer. */ providerKey?: AiReviewProviderKey | null | undefined; + /** #8121: optional explicit confidence floor. Absent ⇒ the run resolves the live backtest-gated override + * itself (getSatisfactionFloorOverride; null when the autotune flag is off), falling back to the pure + * module's shipped constant — so every caller gets the loosened floor with zero threading. */ + confidenceFloor?: number | undefined; }; export type LinkedIssueSatisfactionRunResult = @@ -72,6 +77,7 @@ async function runWorkersSatisfactionOpinion( system: string, user: string, maxTokens: number, + confidenceFloor?: number, ): Promise { const ai = env.AI as unknown as AiRunner | undefined; if (!ai || typeof ai.run !== "function") return { result: null }; @@ -86,7 +92,7 @@ async function runWorkersSatisfactionOpinion( extra, ); const text = coerceAiText(raw); - const result = buildLinkedIssueSatisfactionResult(issueText, text); + const result = buildLinkedIssueSatisfactionResult(issueText, text, confidenceFloor); if (result) return { result, usage: coerceAiUsage(raw), rawText: text }; } catch (error) { if (isRateLimitError(error)) break; @@ -109,6 +115,11 @@ export async function runLoopOverLinkedIssueSatisfaction(env: Env, input: Linked // nothing to assess, so short-circuit before spending any budget or making a model call. if (!(input.issueText ?? "").trim()) return { status: "ok", result: null, estimatedNeurons: 0 }; + // #8121: resolve the live backtest-gated floor override HERE (single resolution point for every caller) + // unless the caller supplied an explicit floor. Null (flag off / no valid override) keeps the pure + // module's shipped constant via the parameter default -- byte-identical to pre-#8121 behavior. + const confidenceFloor = input.confidenceFloor ?? (await getSatisfactionFloorOverride(env)) ?? undefined; + const maxTokens = clampNumber(Number(env.AI_MAX_OUTPUT_TOKENS || 256), 256, 1024); const user = buildLinkedIssueSatisfactionPrompt({ issueText: input.issueText, @@ -146,11 +157,11 @@ export async function runLoopOverLinkedIssueSatisfaction(env: Env, input: Linked let rawModelText: string | undefined; if (input.providerKey) { const { text, usage: byokUsage } = await callAiProvider(input.providerKey, SATISFACTION_SYSTEM_PROMPT, user, maxTokens); - result = text ? buildLinkedIssueSatisfactionResult(input.issueText, text) : null; + result = text ? buildLinkedIssueSatisfactionResult(input.issueText, text, confidenceFloor) : null; usage = byokUsage; rawModelText = text || undefined; } else { - ({ result, usage, rawText: rawModelText } = await runWorkersSatisfactionOpinion(env, input.issueText, SATISFACTION_SYSTEM_PROMPT, user, maxTokens)); + ({ result, usage, rawText: rawModelText } = await runWorkersSatisfactionOpinion(env, input.issueText, SATISFACTION_SYSTEM_PROMPT, user, maxTokens, confidenceFloor)); } 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); return { status: "ok", result, estimatedNeurons, ...(rawModelText ? { rawModelText } : {}) }; diff --git a/src/services/linked-issue-satisfaction.ts b/src/services/linked-issue-satisfaction.ts index 83c5e6adca..2e59007b98 100644 --- a/src/services/linked-issue-satisfaction.ts +++ b/src/services/linked-issue-satisfaction.ts @@ -112,7 +112,12 @@ export function buildLinkedIssueSatisfactionPrompt(input: LinkedIssueSatisfactio /** Parse the model's raw JSON text response into a {@link LinkedIssueSatisfactionResult}, or null when the * output is unusable (no JSON object, invalid status, or the confidence floor rejects an "unaddressed" call). * PURE — never throws (a malformed blob that matches the brace regex but fails JSON.parse is caught). */ -export function parseLinkedIssueSatisfactionOpinion(text: string): LinkedIssueSatisfactionResult | null { +export function parseLinkedIssueSatisfactionOpinion( + text: string, + // #8121: the floor is overridable ONLY downward and only via the backtest-gated loosening loop -- callers + // without an override pass nothing and get the shipped constant, byte-identical to before. + confidenceFloor: number = LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR, +): LinkedIssueSatisfactionResult | null { const match = text .replace(/^```(?:json)?\s*/i, "") .replace(/```$/i, "") @@ -129,7 +134,7 @@ export function parseLinkedIssueSatisfactionOpinion(text: string): LinkedIssueSa const confidence = parseConfidence(obj.confidence); // Fail-safe floor (#2172): a low-confidence "unaddressed" is never published as unaddressed — the caller // gets no finding at all rather than a shaky "you didn't fix this" call. addressed/partial are unaffected. - if (obj.status === "unaddressed" && confidence < LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR) return null; + if (obj.status === "unaddressed" && confidence < confidenceFloor) return null; if (!rationale) return null; return { status: obj.status, rationale, confidence }; } @@ -143,10 +148,11 @@ export function parseLinkedIssueSatisfactionOpinion(text: string): LinkedIssueSa export function buildLinkedIssueSatisfactionResult( issueText: string | null | undefined, modelResponseText: string, + confidenceFloor: number = LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR, ): LinkedIssueSatisfactionResult | null { if (!(issueText ?? "").trim()) return null; try { - const opinion = parseLinkedIssueSatisfactionOpinion(modelResponseText); + const opinion = parseLinkedIssueSatisfactionOpinion(modelResponseText, confidenceFloor); if (!opinion) return null; const safeRationale = toPublicSafe(opinion.rationale); if (!safeRationale) return null; diff --git a/src/services/satisfaction-floor-loosening-run.ts b/src/services/satisfaction-floor-loosening-run.ts new file mode 100644 index 0000000000..809f3b6781 --- /dev/null +++ b/src/services/satisfaction-floor-loosening-run.ts @@ -0,0 +1,103 @@ +// IO orchestration for the backtest-gated satisfaction-floor loosening (#8121 narrow start) — the +// "separate, I/O-touching slice" satisfaction-floor-loosening.ts leaves to its caller, mirroring +// threshold-backtest-run.ts's identical split. Three responsibilities: +// 1. read the live floor override (system_flags, migration 0054 — the same operational-flag table the +// auto-tune circuit breakers use, so no new storage surface); +// 2. evaluate a loosening against the rule's real recorded history (SignalStore → corpus → pure core); +// 3. apply an approved proposal: write the override + a calibration audit event. +// +// The ENTIRE apply path is flag-gated on env.SATISFACTION_FLOOR_AUTOTUNE_ENABLED (wrangler var, unset/false +// by default), so a deploy without the flag is behavior-identical — #8121's Boundaries demand no autonomous +// config change without the explicit opt-in. Direction is enforced here AGAIN (proposed < current, ≥ hard +// minimum) on top of the evaluator's own guarantee: the write path must be independently incapable of +// tightening-disguised-as-loosening or of sailing past the safety minimum, whatever its input claims. +import { buildBacktestCorpus } from "@loopover/engine"; +import { createSignalStore } from "../review/signal-tracking-wire"; +import { recordAuditEvent } from "../db/repositories"; +import { + evaluateSatisfactionFloorLoosening, + SATISFACTION_FLOOR_HARD_MINIMUM, + SATISFACTION_FLOOR_RULE_ID, + type SatisfactionFloorLooseningProposal, +} from "./satisfaction-floor-loosening"; +import { LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR } from "./linked-issue-satisfaction"; + +export const SATISFACTION_FLOOR_OVERRIDE_FLAG_KEY = "satisfaction_floor_override"; +export const SATISFACTION_FLOOR_LOOSENING_EVENT_TYPE = "calibration.satisfaction_floor_loosened"; +const CORPUS_LOOKBACK_MS = 90 * 24 * 60 * 60 * 1000; // mirrors threshold-backtest-run's 90-day window + +/** Truthy-string env flag, matching the repo's flag convention (mirrors outcomes-wire's flagTruthy). */ +export function isSatisfactionFloorAutotuneEnabled(env: Env): boolean { + const value = (env.SATISFACTION_FLOOR_AUTOTUNE_ENABLED ?? "").trim().toLowerCase(); + return value === "1" || value === "true" || value === "on" || value === "yes"; +} + +/** + * Read the live floor override. Returns null (caller uses the shipped default) when: the autotune flag is + * off (an operator turning the feature off instantly restores the shipped floor, no cleanup required), no + * override row exists, or the stored value fails validation — an override may only ever sit BELOW the + * shipped floor and AT/ABOVE the hard minimum, so a corrupted/hand-edited row can never tighten the floor + * or loosen it past safety. Fail-safe null on any DB error (the shipped default is always the fallback). + */ +export async function getSatisfactionFloorOverride(env: Env): Promise { + if (!isSatisfactionFloorAutotuneEnabled(env)) return null; + try { + const row = await env.DB.prepare("SELECT value FROM system_flags WHERE key = ?") + .bind(SATISFACTION_FLOOR_OVERRIDE_FLAG_KEY) + .first<{ value: string }>(); + if (!row) return null; + const parsed = Number(row.value); + if (!Number.isFinite(parsed) || parsed >= LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR || parsed < SATISFACTION_FLOOR_HARD_MINIMUM) { + return null; + } + return parsed; + } catch { + return null; + } +} + +export type SatisfactionFloorLooseningRunResult = + | { applied: false; reason: "flag_off" | "no_proposal" | "already_applied" } + | { applied: true; proposal: SatisfactionFloorLooseningProposal }; + +/** + * Evaluate and (when justified) apply a backtest-gated loosening of the satisfaction floor. The current + * floor is the live override when one exists (so repeated runs evaluate from where the system actually is, + * stepping at most one candidate per run, and can never oscillate upward). Persists the new override plus a + * `calibration.satisfaction_floor_loosened` audit event carrying both split comparisons — the same + * structured evidence trail every other calibration write in epic #8082 leaves. Audit write is best-effort; + * the override write is NOT (an unrecorded floor change would be worse than no change, so a failed flag + * write aborts by throwing to the caller — the internal route surfaces it as a 500). + */ +export async function runSatisfactionFloorLoosening(env: Env, nowMs: number = Date.now()): Promise { + if (!isSatisfactionFloorAutotuneEnabled(env)) return { applied: false, reason: "flag_off" }; + + const currentFloor = (await getSatisfactionFloorOverride(env)) ?? LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR; + if (currentFloor <= SATISFACTION_FLOOR_HARD_MINIMUM) return { applied: false, reason: "already_applied" }; + + const { fired, overrides } = await createSignalStore(env).queryRuleHistory(SATISFACTION_FLOOR_RULE_ID, nowMs - CORPUS_LOOKBACK_MS); + const cases = buildBacktestCorpus(SATISFACTION_FLOOR_RULE_ID, fired, overrides); + const proposal = evaluateSatisfactionFloorLoosening(cases, currentFloor); + if (!proposal) return { applied: false, reason: "no_proposal" }; + // Defense in depth: the write path independently refuses anything that isn't a strict, bounded loosening. + if (proposal.proposedFloor >= currentFloor || proposal.proposedFloor < SATISFACTION_FLOOR_HARD_MINIMUM) { + return { applied: false, reason: "no_proposal" }; + } + + await env.DB.prepare( + "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", + ) + .bind(SATISFACTION_FLOOR_OVERRIDE_FLAG_KEY, String(proposal.proposedFloor)) + .run(); + + await recordAuditEvent(env, { + eventType: SATISFACTION_FLOOR_LOOSENING_EVENT_TYPE, + actor: "loopover", + targetKey: SATISFACTION_FLOOR_RULE_ID, + outcome: "completed", + detail: `satisfaction confidence floor loosened ${proposal.currentFloor} -> ${proposal.proposedFloor} (backtest-gated, visible improved + held-out non-regressed)`, + metadata: { proposal }, + }).catch(() => undefined); + + return { applied: true, proposal }; +} diff --git a/src/services/satisfaction-floor-loosening.ts b/src/services/satisfaction-floor-loosening.ts new file mode 100644 index 0000000000..1715a92040 --- /dev/null +++ b/src/services/satisfaction-floor-loosening.ts @@ -0,0 +1,93 @@ +// Backtest-gated loosening of the linked-issue satisfaction confidence floor (#8121's approved narrow +// start, epic #8082). auto-tune.ts's OverridePayload doc states the historical rule plainly: "a loosening +// recommendation never carries a payload (autonomous loosening is the regression risk the loop exists to +// avoid)". The #8082 backtest primitives are exactly the missing risk measurement that comment names: this +// module proposes a LOWER value for LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR when — and only when — the +// proposal clears the Pareto floor (#8086) against real human-adjudicated history on the VISIBLE split AND +// does not regress on the deterministic HELD-OUT split (#8087), so a loosening can never be hand-tuned to +// just the incidents already known about. +// +// PURE: no IO, no env, no clock — the corpus is the caller's (satisfaction-floor-loosening-run.ts reads it +// via the SignalStore). Scoped to exactly this one scalar; generalizing to other loosenable knobs is the +// rest of epic #8121, decomposed separately, and requires its own explicit approval per that epic's +// Boundaries. +import { + buildConfidenceThresholdClassifier, + compareBacktestScores, + scoreBacktest, + splitBacktestCorpus, + type BacktestCase, + type BacktestComparison, +} from "@loopover/engine"; +import { LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR } from "./linked-issue-satisfaction"; + +export const SATISFACTION_FLOOR_RULE_ID = "linked_issue_scope_mismatch"; + +/** Candidate loosened floors, nearest-to-current first — the FIRST candidate that clears both splits wins, + * so the loop always takes the SMALLEST loosening step the evidence supports, never the biggest. */ +export const SATISFACTION_FLOOR_LOOSENING_CANDIDATES: readonly number[] = [0.45, 0.4, 0.35, 0.3]; + +/** Hard safety minimum — no backtest result, however good, may loosen the floor below this. A floor of ~0 + * would republish every hallucinated low-confidence "unaddressed" call, the exact failure mode the floor + * exists to suppress (see LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR's own doc). */ +export const SATISFACTION_FLOOR_HARD_MINIMUM = 0.3; + +/** Below these decided-sample sizes the corpus cannot honestly justify ANY loosening — mirrors + * AUTOTUNE_MIN_DECIDED's "never on noise" discipline (auto-tune.ts). */ +export const SATISFACTION_FLOOR_MIN_VISIBLE_CASES = 20; +export const SATISFACTION_FLOOR_MIN_HELD_OUT_CASES = 5; + +export const SATISFACTION_FLOOR_HELD_OUT_FRACTION = 0.25; +/** Fixed split seed: the held-out membership must never reshuffle between evaluations, or a repeatedly-run + * loop could fish for a lucky split (see splitBacktestCorpus's own determinism contract). */ +export const SATISFACTION_FLOOR_SPLIT_SEED = "satisfaction-floor-loosening-v1"; + +export type SatisfactionFloorLooseningProposal = { + ruleId: typeof SATISFACTION_FLOOR_RULE_ID; + currentFloor: number; + proposedFloor: number; + visibleCases: number; + heldOutCases: number; + visible: BacktestComparison; + heldOut: BacktestComparison; +}; + +/** + * Evaluate whether the satisfaction confidence floor can be safely loosened, per #8121's approved gate: + * the smallest candidate step below `currentFloor` (never below {@link SATISFACTION_FLOOR_HARD_MINIMUM}) + * whose backtest verdict is strictly `"improved"` on the visible split AND non-`"regressed"` on the + * held-out split. Returns null when the corpus is too small, no candidate qualifies, or `currentFloor` is + * already at/below the hard minimum (a previously-applied loosening never compounds past it). Pure and + * deterministic — same corpus + floor ⇒ same proposal. + */ +export function evaluateSatisfactionFloorLoosening( + cases: readonly BacktestCase[], + currentFloor: number = LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR, +): SatisfactionFloorLooseningProposal | null { + const { visible, heldOut } = splitBacktestCorpus(cases, SATISFACTION_FLOOR_HELD_OUT_FRACTION, SATISFACTION_FLOOR_SPLIT_SEED); + if (visible.length < SATISFACTION_FLOOR_MIN_VISIBLE_CASES || heldOut.length < SATISFACTION_FLOOR_MIN_HELD_OUT_CASES) return null; + + for (const candidate of SATISFACTION_FLOOR_LOOSENING_CANDIDATES) { + if (candidate >= currentFloor || candidate < SATISFACTION_FLOOR_HARD_MINIMUM) continue; + const visibleComparison = compareOnSlice(visible, currentFloor, candidate); + if (visibleComparison.verdict !== "improved") continue; + const heldOutComparison = compareOnSlice(heldOut, currentFloor, candidate); + if (heldOutComparison.verdict === "regressed") continue; + return { + ruleId: SATISFACTION_FLOOR_RULE_ID, + currentFloor, + proposedFloor: candidate, + visibleCases: visible.length, + heldOutCases: heldOut.length, + visible: visibleComparison, + heldOut: heldOutComparison, + }; + } + return null; +} + +function compareOnSlice(slice: readonly BacktestCase[], currentFloor: number, candidate: number): BacktestComparison { + const baseline = scoreBacktest(SATISFACTION_FLOOR_RULE_ID, slice, buildConfidenceThresholdClassifier(currentFloor)); + const proposed = scoreBacktest(SATISFACTION_FLOOR_RULE_ID, slice, buildConfidenceThresholdClassifier(candidate)); + return compareBacktestScores(baseline, proposed); +} diff --git a/test/helpers/d1.ts b/test/helpers/d1.ts index 014830df93..6bba3ad9c5 100644 --- a/test/helpers/d1.ts +++ b/test/helpers/d1.ts @@ -87,6 +87,7 @@ export function createTestEnv(overrides: Partial = {}): Env { GITTENSOR_UPSTREAM_REF: "test", GITTENSOR_REGISTRY_URL: "https://raw.githubusercontent.com/entrius/gittensor/test/gittensor/validator/weights/master_repositories.json", LOOPOVER_AUTO_FILE_DRIFT_ISSUES: "false", + SATISFACTION_FLOOR_AUTOTUNE_ENABLED: "false", // Deliberately NOT "JSONbored/gittensory" (the old pre-rename repo name most test fixtures use as their // generic placeholder repoFullName) and NOT "JSONbored/loopover" (the real self-repo default) -- either // would make isLoopOverSelfRepo() accidentally match a fixture that has no intent to exercise self-repo diff --git a/test/unit/linked-issue-satisfaction-run.test.ts b/test/unit/linked-issue-satisfaction-run.test.ts index f5f2fd75bd..7ee90fc8a3 100644 --- a/test/unit/linked-issue-satisfaction-run.test.ts +++ b/test/unit/linked-issue-satisfaction-run.test.ts @@ -1107,3 +1107,40 @@ describe("linked-issue satisfaction wired end-to-end through the real webhook pi expect(gatePatchBody.conclusion).not.toBe("failure"); }); }); + +describe("confidence-floor override threading (#8121)", () => { + const unaddressedBorderline = () => satisfactionJson({ status: "unaddressed", confidence: 0.42, rationale: "The diff does not touch the SSE surface at all." }); + + async function setFloorOverride(env: Env, value: string) { + await env.DB.prepare("INSERT INTO system_flags (key, value, updated_at) VALUES ('satisfaction_floor_override', ?, CURRENT_TIMESTAMP)").bind(value).run(); + } + + it("keeps the shipped floor when the autotune flag is off: a 0.42-confidence unaddressed call stays unpublished", async () => { + const run = vi.fn(async () => ({ response: unaddressedBorderline() })); + const env = enabledEnv(run); + await setFloorOverride(env, "0.4"); // present but the flag is off -- must be ignored + const out = await runLoopOverLinkedIssueSatisfaction(env, baseInput); + expect(out).toMatchObject({ status: "ok", result: null }); + }); + + it("publishes the same call once the backtest-gated override lowers the live floor below its confidence", async () => { + const run = vi.fn(async () => ({ response: unaddressedBorderline() })); + const env = enabledEnv(run); + (env as { SATISFACTION_FLOOR_AUTOTUNE_ENABLED?: string }).SATISFACTION_FLOOR_AUTOTUNE_ENABLED = "true"; + await setFloorOverride(env, "0.4"); + const out = await runLoopOverLinkedIssueSatisfaction(env, baseInput); + expect(out.status).toBe("ok"); + if (out.status !== "ok") throw new Error("unreachable"); + expect(out.result).toMatchObject({ status: "unaddressed", confidence: 0.42 }); + }); + + it("an explicit input.confidenceFloor wins over the stored override", async () => { + const run = vi.fn(async () => ({ response: unaddressedBorderline() })); + const env = enabledEnv(run); + (env as { SATISFACTION_FLOOR_AUTOTUNE_ENABLED?: string }).SATISFACTION_FLOOR_AUTOTUNE_ENABLED = "true"; + await setFloorOverride(env, "0.3"); + // Explicit floor ABOVE the call's confidence: the override (0.3) would publish it; the input floor must win. + const out = await runLoopOverLinkedIssueSatisfaction(env, { ...baseInput, confidenceFloor: 0.45 }); + expect(out).toMatchObject({ status: "ok", result: null }); + }); +}); diff --git a/test/unit/linked-issue-satisfaction.test.ts b/test/unit/linked-issue-satisfaction.test.ts index 1a089db2eb..939e91a5c6 100644 --- a/test/unit/linked-issue-satisfaction.test.ts +++ b/test/unit/linked-issue-satisfaction.test.ts @@ -162,3 +162,26 @@ describe("buildLinkedIssueSatisfactionResult", () => { expect(buildLinkedIssueSatisfactionResult("Fix the crash", null as unknown as string)).toBeNull(); }); }); + +describe("confidence-floor parameter (#8121)", () => { + const borderline = JSON.stringify({ status: "unaddressed", rationale: "The diff never touches the asked-for surface.", confidence: 0.42 }); + + it("parse: the default floor drops a 0.42-confidence unaddressed call; an explicitly loosened floor publishes it", () => { + expect(parseLinkedIssueSatisfactionOpinion(borderline)).toBeNull(); + const loosened = parseLinkedIssueSatisfactionOpinion(borderline, 0.4); + expect(loosened).toMatchObject({ status: "unaddressed", confidence: 0.42 }); + // A floor param can also be HIGHER when a caller passes one explicitly -- the parse applies whatever it + // is given; direction policy lives in the loosening loop's own guards, not here. + expect(parseLinkedIssueSatisfactionOpinion(JSON.stringify({ status: "unaddressed", rationale: "r", confidence: 0.6 }), 0.7)).toBeNull(); + }); + + it("build: threads the floor through to the parse (default drops, loosened publishes)", () => { + expect(buildLinkedIssueSatisfactionResult("real issue text", borderline)).toBeNull(); + expect(buildLinkedIssueSatisfactionResult("real issue text", borderline, 0.4)).toMatchObject({ status: "unaddressed" }); + }); + + it("the floor never gates addressed/partial verdicts, with or without an override", () => { + const partial = JSON.stringify({ status: "partial", rationale: "Half of the ask is delivered.", confidence: 0.1 }); + expect(parseLinkedIssueSatisfactionOpinion(partial, 0.9)).toMatchObject({ status: "partial" }); + }); +}); diff --git a/test/unit/routes-satisfaction-floor-loosening.test.ts b/test/unit/routes-satisfaction-floor-loosening.test.ts new file mode 100644 index 0000000000..05195b678e --- /dev/null +++ b/test/unit/routes-satisfaction-floor-loosening.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { createApp } from "../../src/api/routes"; +import { createTestEnv } from "../helpers/d1"; + +// POST /v1/internal/calibration/loosen-satisfaction-floor (#8121 narrow start): the manual trigger for one +// backtest-gated loosening evaluation. Mirrors routes-internal-decision-calibration.test.ts's bearer-gate +// pattern; the loop's own behavior is covered in satisfaction-floor-loosening-run.test.ts — this file pins +// the route's flag-gate, auth, and response shape only. + +const bearer = (env: Env) => ({ authorization: `Bearer ${env.INTERNAL_JOB_TOKEN}` }); +const enabledEnv = () => createTestEnv({ SATISFACTION_FLOOR_AUTOTUNE_ENABLED: "true" as never }); + +describe("POST /v1/internal/calibration/loosen-satisfaction-floor (#8121)", () => { + it("404s when the autotune flag is off — the endpoint does not exist on a deploy that has not opted in", async () => { + const app = createApp(); + const env = createTestEnv(); + const res = await app.request("/v1/internal/calibration/loosen-satisfaction-floor", { method: "POST", headers: bearer(env) }, env); + expect(res.status).toBe(404); + }); + + it("401s without the internal token (the /v1/internal/* middleware gate), even when the flag is on", async () => { + const app = createApp(); + const env = enabledEnv(); + expect((await app.request("/v1/internal/calibration/loosen-satisfaction-floor", { method: "POST" }, env)).status).toBe(401); + expect( + (await app.request("/v1/internal/calibration/loosen-satisfaction-floor", { method: "POST", headers: { authorization: "Bearer nope" } }, env)).status, + ).toBe(401); + }); + + it("200s with the run result on an empty corpus (no_proposal) — evaluating is always safe, applying is gated", async () => { + const app = createApp(); + const env = enabledEnv(); + const res = await app.request("/v1/internal/calibration/loosen-satisfaction-floor", { method: "POST", headers: bearer(env) }, env); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ applied: false, reason: "no_proposal" }); + }); +}); diff --git a/test/unit/satisfaction-floor-loosening-run.test.ts b/test/unit/satisfaction-floor-loosening-run.test.ts new file mode 100644 index 0000000000..2186f4d7b3 --- /dev/null +++ b/test/unit/satisfaction-floor-loosening-run.test.ts @@ -0,0 +1,176 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { splitBacktestCorpus } from "@loopover/engine"; +import { + getSatisfactionFloorOverride, + isSatisfactionFloorAutotuneEnabled, + runSatisfactionFloorLoosening, + SATISFACTION_FLOOR_LOOSENING_EVENT_TYPE, + SATISFACTION_FLOOR_OVERRIDE_FLAG_KEY, +} from "../../src/services/satisfaction-floor-loosening-run"; +import * as core from "../../src/services/satisfaction-floor-loosening"; +import { LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR } from "../../src/services/linked-issue-satisfaction"; +import { createSignalStore } from "../../src/review/signal-tracking-wire"; +import { listAuditEventsByType } from "../../src/db/repositories"; +import * as repositories from "../../src/db/repositories"; +import { createTestEnv } from "../helpers/d1"; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +const enabledEnv = (overrides: Partial = {}) => createTestEnv({ SATISFACTION_FLOOR_AUTOTUNE_ENABLED: "true" as never, ...overrides }); + +async function setOverrideRow(env: Env, value: string) { + await env.DB.prepare("INSERT INTO system_flags (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = excluded.value") + .bind(SATISFACTION_FLOOR_OVERRIDE_FLAG_KEY, value) + .run(); +} + +describe("isSatisfactionFloorAutotuneEnabled (#8121)", () => { + it("accepts the repo's truthy-string spellings and rejects everything else, including absent", () => { + for (const value of ["1", "true", "on", "yes", " TRUE "]) { + expect(isSatisfactionFloorAutotuneEnabled(createTestEnv({ SATISFACTION_FLOOR_AUTOTUNE_ENABLED: value as never }))).toBe(true); + } + for (const value of ["false", "0", "", "off", undefined]) { + expect(isSatisfactionFloorAutotuneEnabled(createTestEnv({ SATISFACTION_FLOOR_AUTOTUNE_ENABLED: value as never }))).toBe(false); + } + }); +}); + +describe("getSatisfactionFloorOverride (#8121)", () => { + it("returns null when the autotune flag is off — flipping the flag off instantly restores the shipped floor", async () => { + const env = createTestEnv(); + await setOverrideRow(env, "0.4"); + expect(await getSatisfactionFloorOverride(env)).toBeNull(); + }); + + it("returns a valid stored override, and null when no row exists", async () => { + const env = enabledEnv(); + expect(await getSatisfactionFloorOverride(env)).toBeNull(); + await setOverrideRow(env, "0.4"); + expect(await getSatisfactionFloorOverride(env)).toBe(0.4); + }); + + it("rejects values that would tighten (>= shipped floor), pass the hard minimum, or fail to parse", async () => { + const env = enabledEnv(); + for (const bad of [String(LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR), "0.9", String(core.SATISFACTION_FLOOR_HARD_MINIMUM - 0.05), "not-a-number"]) { + await setOverrideRow(env, bad); + expect(await getSatisfactionFloorOverride(env)).toBeNull(); + } + }); + + it("fails safe to null on a DB error", async () => { + const env = enabledEnv(); + env.DB = { prepare: () => { throw new Error("boom"); } } as never; + expect(await getSatisfactionFloorOverride(env)).toBeNull(); + }); +}); + +describe("runSatisfactionFloorLoosening (#8121)", () => { + async function seedLooseningFriendlyHistory(env: Env) { + // Same membership-probe technique as the pure-core suite: ask the real splitter which keys land where, + // then seed borderline-confirmed history in both slices so 0.5 -> 0.45 clears both gates. + const pool = Array.from({ length: 120 }, (_, i) => `acme/widgets#${i + 1}`); + const probe = pool.map((targetKey) => ({ + ruleId: core.SATISFACTION_FLOOR_RULE_ID, + targetKey, + outcome: "unaddressed", + label: "confirmed" as const, + firedAt: "2026-07-01T00:00:00.000Z", + decidedAt: "2026-07-02T00:00:00.000Z", + })); + const { visible, heldOut } = splitBacktestCorpus(probe, core.SATISFACTION_FLOOR_HELD_OUT_FRACTION, core.SATISFACTION_FLOOR_SPLIT_SEED); + const store = createSignalStore(env); + const now = Date.now(); + const keys = [ + ...visible.slice(0, core.SATISFACTION_FLOOR_MIN_VISIBLE_CASES + 4).map((c) => c.targetKey), + ...heldOut.slice(0, core.SATISFACTION_FLOOR_MIN_HELD_OUT_CASES + 2).map((c) => c.targetKey), + ]; + for (const [i, targetKey] of keys.entries()) { + await store.recordRuleFired({ + ruleId: core.SATISFACTION_FLOOR_RULE_ID, + targetKey, + outcome: "unaddressed", + occurredAt: new Date(now - 10_000 - i).toISOString(), + metadata: { confidence: 0.47 }, + }); + await store.recordHumanOverride({ ruleId: core.SATISFACTION_FLOOR_RULE_ID, targetKey, verdict: "confirmed", occurredAt: new Date(now - i).toISOString() }); + } + // One genuinely-reversed deep-low-confidence firing per slice, mirroring the pure-core fixture: a true + // positive must exist on both sides of the comparison or the candidate's precision denominator is 0 + // (null) and the improvement can't register on any axis. + for (const targetKey of [visible[core.SATISFACTION_FLOOR_MIN_VISIBLE_CASES + 5]!.targetKey, heldOut[core.SATISFACTION_FLOOR_MIN_HELD_OUT_CASES + 3]!.targetKey]) { + await store.recordRuleFired({ + ruleId: core.SATISFACTION_FLOOR_RULE_ID, + targetKey, + outcome: "unaddressed", + occurredAt: new Date(now - 20_000).toISOString(), + metadata: { confidence: 0.1 }, + }); + await store.recordHumanOverride({ ruleId: core.SATISFACTION_FLOOR_RULE_ID, targetKey, verdict: "reversed", occurredAt: new Date(now - 5000).toISOString() }); + } + } + + it("returns flag_off without touching anything when the autotune flag is not set", async () => { + expect(await runSatisfactionFloorLoosening(createTestEnv())).toEqual({ applied: false, reason: "flag_off" }); + }); + + it("returns no_proposal on an empty corpus", async () => { + expect(await runSatisfactionFloorLoosening(enabledEnv())).toEqual({ applied: false, reason: "no_proposal" }); + }); + + it("applies a backtest-cleared loosening: writes the override row + the calibration audit event", async () => { + const env = enabledEnv(); + await seedLooseningFriendlyHistory(env); + const result = await runSatisfactionFloorLoosening(env); + expect(result.applied).toBe(true); + if (!result.applied) throw new Error("unreachable"); + expect(result.proposal.proposedFloor).toBe(core.SATISFACTION_FLOOR_LOOSENING_CANDIDATES[0]); + + expect(await getSatisfactionFloorOverride(env)).toBe(core.SATISFACTION_FLOOR_LOOSENING_CANDIDATES[0]); + const audits = await listAuditEventsByType(env, SATISFACTION_FLOOR_LOOSENING_EVENT_TYPE, new Date(Date.now() - 60_000).toISOString()); + expect(audits).toHaveLength(1); + expect(audits[0]!.metadata.proposal).toMatchObject({ currentFloor: 0.5, proposedFloor: core.SATISFACTION_FLOOR_LOOSENING_CANDIDATES[0] }); + }); + + it("re-evaluates from the CURRENT (already-loosened) floor — one candidate step per run, never oscillating upward", async () => { + const env = enabledEnv(); + await seedLooseningFriendlyHistory(env); + expect((await runSatisfactionFloorLoosening(env)).applied).toBe(true); + // Second run: current floor is now 0.45; the same corpus (borderline 0.47 cases sit ABOVE it) offers no + // further improvement, so nothing more is applied and the override stays where it was. + expect(await runSatisfactionFloorLoosening(env)).toEqual({ applied: false, reason: "no_proposal" }); + expect(await getSatisfactionFloorOverride(env)).toBe(core.SATISFACTION_FLOOR_LOOSENING_CANDIDATES[0]); + }); + + it("audit write is best-effort: a failed audit record never blocks an applied loosening", async () => { + const env = enabledEnv(); + await seedLooseningFriendlyHistory(env); + vi.spyOn(repositories, "recordAuditEvent").mockRejectedValue(new Error("audit down")); + const result = await runSatisfactionFloorLoosening(env); + expect(result.applied).toBe(true); + expect(await getSatisfactionFloorOverride(env)).toBe(core.SATISFACTION_FLOOR_LOOSENING_CANDIDATES[0]); + }); + + it("returns already_applied once the live floor sits at/below the hard minimum", async () => { + const env = enabledEnv(); + await setOverrideRow(env, String(core.SATISFACTION_FLOOR_HARD_MINIMUM)); + expect(await runSatisfactionFloorLoosening(env)).toEqual({ applied: false, reason: "already_applied" }); + }); + + it("defense-in-depth: refuses a proposal that is not a strict bounded loosening, whatever the evaluator claims", async () => { + const env = enabledEnv(); + const bogus: core.SatisfactionFloorLooseningProposal = { + ruleId: core.SATISFACTION_FLOOR_RULE_ID, + currentFloor: 0.5, + proposedFloor: 0.6, // tightening disguised as a proposal — must be refused by the write path itself + visibleCases: 100, + heldOutCases: 30, + visible: {} as never, + heldOut: {} as never, + }; + vi.spyOn(core, "evaluateSatisfactionFloorLoosening").mockReturnValue(bogus); + expect(await runSatisfactionFloorLoosening(env)).toEqual({ applied: false, reason: "no_proposal" }); + expect(await getSatisfactionFloorOverride(env)).toBeNull(); + }); +}); diff --git a/test/unit/satisfaction-floor-loosening.test.ts b/test/unit/satisfaction-floor-loosening.test.ts new file mode 100644 index 0000000000..fe89e2d68f --- /dev/null +++ b/test/unit/satisfaction-floor-loosening.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from "vitest"; +import { splitBacktestCorpus, type BacktestCase } from "@loopover/engine"; +import { + evaluateSatisfactionFloorLoosening, + SATISFACTION_FLOOR_HARD_MINIMUM, + SATISFACTION_FLOOR_HELD_OUT_FRACTION, + SATISFACTION_FLOOR_LOOSENING_CANDIDATES, + SATISFACTION_FLOOR_MIN_HELD_OUT_CASES, + SATISFACTION_FLOOR_MIN_VISIBLE_CASES, + SATISFACTION_FLOOR_RULE_ID, + SATISFACTION_FLOOR_SPLIT_SEED, +} from "../../src/services/satisfaction-floor-loosening"; + +// Fixture strategy: splitBacktestCorpus assigns held-out membership deterministically by +// (seed, ruleId, targetKey), so the test FIRST asks the real splitter which of a pool of keys land in +// which slice, THEN assigns each case's confidence/label per slice — no reimplementing the hash, no +// hoping a hardcoded key set splits a particular way. +function corpusCase(targetKey: string, confidence: number, label: "reversed" | "confirmed"): BacktestCase { + return { + ruleId: SATISFACTION_FLOOR_RULE_ID, + targetKey, + outcome: "unaddressed", + label, + firedAt: "2026-07-01T00:00:00.000Z", + decidedAt: "2026-07-02T00:00:00.000Z", + metadata: { confidence }, + }; +} + +function sliceMembership(keys: string[]): { visibleKeys: string[]; heldOutKeys: string[] } { + const probe = keys.map((key) => corpusCase(key, 0.9, "confirmed")); + const { visible, heldOut } = splitBacktestCorpus(probe, SATISFACTION_FLOOR_HELD_OUT_FRACTION, SATISFACTION_FLOOR_SPLIT_SEED); + return { visibleKeys: visible.map((c) => c.targetKey), heldOutKeys: heldOut.map((c) => c.targetKey) }; +} + +const POOL = Array.from({ length: 120 }, (_, i) => `acme/widgets#${i + 1}`); +const { visibleKeys, heldOutKeys } = sliceMembership(POOL); + +/** Build a corpus where every case in BOTH slices supports loosening 0.5 → 0.45: confidence 0.47 firings a + * human CONFIRMED (baseline floor 0.5 predicts them "reversed" — false positives; candidate 0.45 predicts + * "confirmed" — precision improves) plus anchor true-positives below every candidate so recall never moves. */ +function looseningFriendlyCorpus(): BacktestCase[] { + const cases: BacktestCase[] = []; + for (const key of visibleKeys.slice(0, SATISFACTION_FLOOR_MIN_VISIBLE_CASES + 4)) { + cases.push(corpusCase(key, 0.47, "confirmed")); + } + for (const key of heldOutKeys.slice(0, SATISFACTION_FLOOR_MIN_HELD_OUT_CASES + 2)) { + cases.push(corpusCase(key, 0.47, "confirmed")); + } + // One genuine reversed case per slice, far below every candidate, so precision/recall stay comparable. + cases.push(corpusCase(visibleKeys[SATISFACTION_FLOOR_MIN_VISIBLE_CASES + 5]!, 0.1, "reversed")); + cases.push(corpusCase(heldOutKeys[SATISFACTION_FLOOR_MIN_HELD_OUT_CASES + 3]!, 0.1, "reversed")); + return cases; +} + +describe("evaluateSatisfactionFloorLoosening (#8121)", () => { + it("proposes the SMALLEST loosening step when both splits support it, with full comparison evidence", () => { + const proposal = evaluateSatisfactionFloorLoosening(looseningFriendlyCorpus()); + expect(proposal).not.toBeNull(); + expect(proposal!.proposedFloor).toBe(SATISFACTION_FLOOR_LOOSENING_CANDIDATES[0]); + expect(proposal!.currentFloor).toBe(0.5); + expect(proposal!.visible.verdict).toBe("improved"); + expect(proposal!.heldOut.verdict).not.toBe("regressed"); + expect(proposal!.visibleCases).toBeGreaterThanOrEqual(SATISFACTION_FLOOR_MIN_VISIBLE_CASES); + expect(proposal!.heldOutCases).toBeGreaterThanOrEqual(SATISFACTION_FLOOR_MIN_HELD_OUT_CASES); + expect(proposal!.ruleId).toBe(SATISFACTION_FLOOR_RULE_ID); + }); + + it("is deterministic: identical corpus ⇒ identical proposal", () => { + const corpus = looseningFriendlyCorpus(); + expect(evaluateSatisfactionFloorLoosening(corpus)).toEqual(evaluateSatisfactionFloorLoosening(corpus)); + }); + + it("returns null on a too-small visible slice and on a too-small held-out slice — never loosens on noise", () => { + const visibleOnly = visibleKeys.slice(0, SATISFACTION_FLOOR_MIN_VISIBLE_CASES + 4).map((key) => corpusCase(key, 0.47, "confirmed")); + expect(evaluateSatisfactionFloorLoosening(visibleOnly)).toBeNull(); // held-out side empty + const heldOutOnly = heldOutKeys.slice(0, SATISFACTION_FLOOR_MIN_HELD_OUT_CASES + 2).map((key) => corpusCase(key, 0.47, "confirmed")); + expect(evaluateSatisfactionFloorLoosening(heldOutOnly)).toBeNull(); // visible side too small + expect(evaluateSatisfactionFloorLoosening([])).toBeNull(); + }); + + it("returns null when no candidate improves the visible split (loosening would republish reversed calls)", () => { + // Every borderline firing was REVERSED by a human: baseline (0.5) correctly predicts them reversed + // (true positives); every candidate turns them into false negatives — recall regresses, nothing passes. + const cases: BacktestCase[] = [ + ...visibleKeys.slice(0, SATISFACTION_FLOOR_MIN_VISIBLE_CASES + 4).map((key) => corpusCase(key, 0.47, "reversed")), + ...heldOutKeys.slice(0, SATISFACTION_FLOOR_MIN_HELD_OUT_CASES + 2).map((key) => corpusCase(key, 0.47, "reversed")), + ]; + expect(evaluateSatisfactionFloorLoosening(cases)).toBeNull(); + }); + + it("rejects a candidate the HELD-OUT split regresses on, even when the visible split improves", () => { + // Visible slice: confirmed borderline firings (loosening improves precision). Held-out slice: REVERSED + // borderline firings (loosening loses real catches — recall regresses). The overfitting guard must veto. + const cases: BacktestCase[] = [ + ...visibleKeys.slice(0, SATISFACTION_FLOOR_MIN_VISIBLE_CASES + 4).map((key) => corpusCase(key, 0.47, "confirmed")), + corpusCase(visibleKeys[SATISFACTION_FLOOR_MIN_VISIBLE_CASES + 5]!, 0.1, "reversed"), + ...heldOutKeys.slice(0, SATISFACTION_FLOOR_MIN_HELD_OUT_CASES + 2).map((key) => corpusCase(key, 0.47, "reversed")), + ]; + expect(evaluateSatisfactionFloorLoosening(cases)).toBeNull(); + }); + + it("never proposes below the hard minimum, and never 'loosens' upward from an already-loosened floor", () => { + // currentFloor already at the hard minimum: every candidate is >= currentFloor or < hard minimum. + expect(evaluateSatisfactionFloorLoosening(looseningFriendlyCorpus(), SATISFACTION_FLOOR_HARD_MINIMUM)).toBeNull(); + // currentFloor between candidates (0.4): candidates 0.45 (>= current, skipped), 0.35/0.3 evaluated only. + const corpus35: BacktestCase[] = [ + ...visibleKeys.slice(0, SATISFACTION_FLOOR_MIN_VISIBLE_CASES + 4).map((key) => corpusCase(key, 0.37, "confirmed")), + ...heldOutKeys.slice(0, SATISFACTION_FLOOR_MIN_HELD_OUT_CASES + 2).map((key) => corpusCase(key, 0.37, "confirmed")), + corpusCase(visibleKeys[SATISFACTION_FLOOR_MIN_VISIBLE_CASES + 5]!, 0.1, "reversed"), + corpusCase(heldOutKeys[SATISFACTION_FLOOR_MIN_HELD_OUT_CASES + 3]!, 0.1, "reversed"), + ]; + const proposal = evaluateSatisfactionFloorLoosening(corpus35, 0.4); + expect(proposal).not.toBeNull(); + expect(proposal!.proposedFloor).toBe(0.35); + expect(proposal!.currentFloor).toBe(0.4); + expect(proposal!.proposedFloor).toBeGreaterThanOrEqual(SATISFACTION_FLOOR_HARD_MINIMUM); + }); +}); diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index c318f582f3..3889c5c7e6 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: d41cd54bc0c9395266c55f6e42615d90) +// Generated by Wrangler by running `wrangler types` (hash: 2992b08ac2db8a03b3ac164a3b2d0eed) // Runtime types generated with workerd@1.20260701.1 2026-05-28 nodejs_compat interface __BaseEnv_Env { REVIEW_AUDIT: R2Bucket; @@ -12,6 +12,7 @@ interface __BaseEnv_Env { GITTENSOR_UPSTREAM_REF: "test"; GITTENSOR_REGISTRY_URL: "https://raw.githubusercontent.com/entrius/gittensor/test/gittensor/validator/weights/master_repositories.json"; SCORING_TIME_DECAY_ENABLED: "true"; + SATISFACTION_FLOOR_AUTOTUNE_ENABLED: "false"; LOOPOVER_AUTO_FILE_DRIFT_ISSUES: "false"; LOOPOVER_DRIFT_ISSUE_REPO: "JSONbored/loopover"; PUBLIC_API_ORIGIN: "https://api.loopover.ai"; @@ -110,6 +111,7 @@ declare namespace NodeJS { | "PUBLIC_API_ORIGIN" | "PUBLIC_REPO_STATS_ALLOWLIST" | "PUBLIC_SITE_ORIGIN" + | "SATISFACTION_FLOOR_AUTOTUNE_ENABLED" | "SCORING_TIME_DECAY_ENABLED" >> {} } diff --git a/wrangler.jsonc b/wrangler.jsonc index 22e5cd0ee5..92220b816d 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -36,6 +36,11 @@ // hyperparameters (registry scoring.time_decay overlaid on the upstream defaults). A fresh PR is // unaffected (decay 1.0); only aged-PR projections decay. Owner-reviewed; this is the go-live switch. "SCORING_TIME_DECAY_ENABLED": "true", + // #8121 (approved narrow start): backtest-gated autonomous loosening of the linked-issue satisfaction + // confidence floor. OFF by default -- the apply path, the live floor override read, and the internal + // trigger route are all inert until this is flipped, so the deploy is behavior-identical. Flip only + // deliberately; flipping back OFF instantly restores the shipped floor (the override stops being read). + "SATISFACTION_FLOOR_AUTOTUNE_ENABLED": "false", "LOOPOVER_AUTO_FILE_DRIFT_ISSUES": "false", "LOOPOVER_DRIFT_ISSUE_REPO": "JSONbored/loopover", // LoopOver rebrand (issue #4765): full cutover, loopover.ai only. gittensory.aethereal.dev is retired