|
| 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