From d8750946b2ae8bd5cc4142ffa4de9a5353472a33 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:46:46 -0700 Subject: [PATCH] feat(calibration): register the slop gate score as the registry's first ceiling knob, report-only (#8224) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The registry gains the orientation axis #8224's design finding called for: a floor knob (every prior entry) loosens DOWNWARD toward hardMinimum; a ceiling knob — the slop gate blocks when risk/100 >= the value — loosens UPWARD toward a declared hardMaximum, with the classifier math unchanged (both knob families fire on value >= threshold). evaluateKnobLoosening and evaluateKnobDrift branch on orientation (including the drift direction label: above-live is LOOSER for a ceiling), and the structural invariants pin ceiling entries to report_only with no tightening ladder until the live storage generalizes. slop_gate_score enters report-only: shipped 0.60 (the gate constant /100), two raises [0.65, 0.70], hard ceiling 0.70, the registry's strictest 50/12 sample floors — bounds rationale in the entry comment. The generic report-only proposals path picks it up with zero new plumbing, and the knobs endpoint now lists EVERY registry knob labeled by applyMode. quality_gate_score stays out (no global shipped default to anchor on), recorded in the registry comment; the flip-to-live issue gets filed only after proposals with real evidence exist. --- .../src/advisory/gate-advisory.ts | 4 +- src/api/routes.ts | 4 +- src/rules/advisory.ts | 4 +- src/services/knob-loosening-run.ts | 15 +- src/services/loosening-knobs.ts | 70 ++++++++- test/unit/knob-loosening-run.test.ts | 9 +- test/unit/loosening-knobs.test.ts | 147 +++++++++++++++++- 7 files changed, 235 insertions(+), 18 deletions(-) diff --git a/packages/loopover-engine/src/advisory/gate-advisory.ts b/packages/loopover-engine/src/advisory/gate-advisory.ts index f346fb21d..aa54b56f0 100644 --- a/packages/loopover-engine/src/advisory/gate-advisory.ts +++ b/packages/loopover-engine/src/advisory/gate-advisory.ts @@ -38,7 +38,9 @@ function sanitizeForCheckRun(text: string): string { } const DEFAULT_AI_REVIEW_CLOSE_CONFIDENCE = 0.93; -const DEFAULT_SLOP_BLOCK_THRESHOLD = 60; +/** Exported to mirror the src twin (#8224): loopover's LOOSENABLE_KNOBS registry anchors the slop knob's + * shipped value on this constant (divided by 100 onto the corpus's confidence scale). Value unchanged. */ +export const DEFAULT_SLOP_BLOCK_THRESHOLD = 60; export type GateCheckConclusion = "success" | "failure" | "action_required" | "neutral" | "skipped"; diff --git a/src/api/routes.ts b/src/api/routes.ts index 5b8bcf876..7b8fa7907 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -322,7 +322,7 @@ import { loadPublicAccuracyTrend } from "../services/public-accuracy-trend"; import { loadPublicRulePrecision } from "../review/public-rule-precision"; import { loadCalibrationTrend } from "../services/rule-calibration-trend"; import { isSatisfactionFloorAutotuneEnabled, loadSatisfactionFloorStatus, runSatisfactionFloorLoosening } from "../services/satisfaction-floor-loosening-run"; -import { loadLiveKnobStatuses } from "../services/knob-loosening-run"; +import { loadAllKnobStatuses } from "../services/knob-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"; @@ -4840,7 +4840,7 @@ export function createApp() { // The #8161 surface generalized across EVERY live registry knob (#8176): one endpoint, one projector, // per-knob flag state + shipped/live/override values + applied history (both split verdicts). Same // deliberate non-flag-gating and INTERNAL_JOB_TOKEN posture as the satisfaction-floor read above. - app.get("/v1/internal/calibration/knobs", async (c) => c.json({ knobs: await loadLiveKnobStatuses(c.env) })); + app.get("/v1/internal/calibration/knobs", async (c) => c.json({ knobs: await loadAllKnobStatuses(c.env) })); app.post("/v1/internal/jobs/refresh-registry", async (c) => { const message: JobMessage = { type: "refresh-registry", requestedBy: "api" }; diff --git a/src/rules/advisory.ts b/src/rules/advisory.ts index 68b6d8a00..0ec5406aa 100644 --- a/src/rules/advisory.ts +++ b/src/rules/advisory.ts @@ -1227,7 +1227,9 @@ function buildQualityGateWarning(policy: GateCheckPolicy): AdvisoryFinding | nul } // Default block threshold = the `high` band (60), used when a maintainer sets slop: block without a minScore. -const DEFAULT_SLOP_BLOCK_THRESHOLD = 60; +/** Exported for the LOOSENABLE_KNOBS registry (#8224): the slop knob's shipped value anchors on this + * constant (divided by 100 onto the corpus's confidence scale). */ +export const DEFAULT_SLOP_BLOCK_THRESHOLD = 60; function buildSlopGateBlocker(policy: GateCheckPolicy): AdvisoryFinding | null { if (gateMode(policy.slopGateMode) !== "block") return null; diff --git a/src/services/knob-loosening-run.ts b/src/services/knob-loosening-run.ts index 7107476f9..bcdd26e61 100644 --- a/src/services/knob-loosening-run.ts +++ b/src/services/knob-loosening-run.ts @@ -476,6 +476,9 @@ export type KnobRepoOverride = { repoFullName: string; value: number }; export type KnobStatus = { knobId: string; + /** The registry's apply contract for this knob (#8224): report_only knobs surface evidence here and in + * the advisor but their apply path refuses — the operator sees WHAT would move before anything can. */ + applyMode: "live" | "report_only"; flagEnabled: boolean; /** The tighten direction's own flag (#8225) — null for a knob that declares no tightening ladder. */ tightenFlagEnabled: boolean | null; @@ -620,6 +623,7 @@ export async function loadKnobStatus(env: Env, knob: LoosenableKnob): Promise { const statuses: KnobStatus[] = []; for (const knob of knobs) { @@ -642,3 +646,12 @@ export async function loadLiveKnobStatuses(env: Env, knobs: readonly LoosenableK } return statuses; } + +/** EVERY registry knob's status, report-only included (#8224), for GET /v1/internal/calibration/knobs — + * the operator must see a report-only knob's evidence (drift, reliability, proposals-to-be) with its + * applyMode label, not discover it only when someone files the flip-to-live issue. */ +export async function loadAllKnobStatuses(env: Env, knobs: readonly LoosenableKnob[] = Object.values(LOOSENABLE_KNOBS)): Promise { + const statuses: KnobStatus[] = []; + for (const knob of knobs) statuses.push(await loadKnobStatus(env, knob)); + return statuses; +} diff --git a/src/services/loosening-knobs.ts b/src/services/loosening-knobs.ts index cf267a666..c8ca3e98c 100644 --- a/src/services/loosening-knobs.ts +++ b/src/services/loosening-knobs.ts @@ -20,17 +20,28 @@ import { type BacktestComparison, } from "@loopover/engine"; import { LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR } from "./linked-issue-satisfaction"; -import { DEFAULT_AI_REVIEW_CLOSE_CONFIDENCE } from "../rules/advisory"; +import { DEFAULT_AI_REVIEW_CLOSE_CONFIDENCE, DEFAULT_SLOP_BLOCK_THRESHOLD } from "../rules/advisory"; export type LoosenableKnob = { /** Stable id — used in override flag keys, audit events, and advisor labels. Never rename. */ knobId: string; ruleId: string; shippedValue: number; - /** Candidate loosened values, nearest-to-shipped first — the smallest evidence-cleared step wins. */ + /** Which way LOOSER points (#8224): a `floor` knob (a rule fires at/above the value; every pre-#8224 + * entry) loosens DOWNWARD; a `ceiling` knob (a gate blocks at/above the value — slop) loosens UPWARD, + * raising the cap so fewer PRs block. The classifier math is identical either way (both knob families + * fire on value >= threshold, so buildConfidenceThresholdClassifier applies unchanged) — orientation + * only decides which side of shipped the candidates sit on and which hard bound applies. */ + orientation: "floor" | "ceiling"; + /** Candidate loosened values, nearest-to-shipped first — the smallest evidence-cleared step wins. + * Floor knobs: strictly below shipped, descending. Ceiling knobs: strictly above shipped, ascending. */ candidates: readonly number[]; - /** No backtest result, however good, may loosen below this. */ + /** Floor knobs: no backtest result, however good, may loosen below this. Ceiling knobs declare the + * mirror bound in {@link LoosenableKnob.hardMaximum} instead and set this to the shipped value (it + * still bounds the drift pool's tighter side). */ hardMinimum: number; + /** Ceiling knobs only (#8224): no backtest result may loosen (raise) the cap above this. */ + hardMaximum?: number; minVisibleCases: number; minHeldOutCases: number; heldOutFraction: number; @@ -76,6 +87,7 @@ export const LOOSENABLE_KNOBS: Readonly> = Object satisfaction_floor: { knobId: "satisfaction_floor", ruleId: "linked_issue_scope_mismatch", + orientation: "floor", shippedValue: LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR, candidates: [0.45, 0.4, 0.35, 0.3], hardMinimum: 0.3, @@ -99,6 +111,7 @@ export const LOOSENABLE_KNOBS: Readonly> = Object ai_review_close_confidence: { knobId: "ai_review_close_confidence", ruleId: "ai_consensus_defect", + orientation: "floor", shippedValue: DEFAULT_AI_REVIEW_CLOSE_CONFIDENCE, candidates: [0.9, 0.85], hardMinimum: 0.85, @@ -125,6 +138,36 @@ export const LOOSENABLE_KNOBS: Readonly> = Object eventType: "calibration.ai_review_close_confidence_tightened", }, }, + // #8224: the slop gate's block threshold enters REPORT-ONLY — proposals surface with full evidence in + // the advisor and the knobs endpoint; the apply path refuses until a flip-to-live ships as its own + // reviewed change, and per #8224 that issue gets filed only AFTER proposals with real evidence exist. + // The registry's first CEILING knob: the gate blocks when slopRisk/100 >= this value (advisory.ts's + // recordRuleFired writes confidence = risk/100 with shipped DEFAULT_SLOP_BLOCK_THRESHOLD), so LOOSER + // means RAISING the cap. Bounds rationale (tighter than close-confidence's, per the issue): this value + // gates contributor-facing verdicts directly, so two small steps (+0.05, +0.10) and a hard ceiling of + // 0.70 — beyond that a "slop gate" that only blocks 70+/100 risk isn't gating. Sample floors match the + // close-confidence knob's strictest-in-registry 50/12 given score noisiness. + // + // quality_gate_score deliberately does NOT enter (#8224's recorded finding): qualityGateMinScore is + // per-repo nullable with NO global shipped default, and a registry entry anchors the whole discipline + // on the shipped constant. It stays out until a global default exists. + slop_gate_score: { + knobId: "slop_gate_score", + ruleId: "slop_gate_score", + orientation: "ceiling", + shippedValue: DEFAULT_SLOP_BLOCK_THRESHOLD / 100, + candidates: [0.65, 0.7], + hardMinimum: DEFAULT_SLOP_BLOCK_THRESHOLD / 100, + hardMaximum: 0.7, + minVisibleCases: 50, + minHeldOutCases: 12, + heldOutFraction: 0.25, + splitSeed: "slop-gate-loosening-v1", + applyMode: "report_only", + overrideFlagKey: "slop_gate_score_override", + looseningEventType: "calibration.slop_gate_score_loosened", + autotuneEnvVar: "SLOP_GATE_SCORE_AUTOTUNE_ENABLED", + }, }); export type KnobLooseningProposal = { @@ -155,7 +198,13 @@ export function evaluateKnobLoosening( if (visible.length < knob.minVisibleCases || heldOut.length < knob.minHeldOutCases) return null; for (const candidate of knob.candidates) { - if (candidate >= currentValue || candidate < knob.hardMinimum) continue; + // Orientation decides which way "looser" points (#8224): floor knobs step DOWN toward hardMinimum, + // ceiling knobs step UP toward hardMaximum. Same evidence discipline either way. + const loosens = + knob.orientation === "ceiling" + ? candidate > currentValue && candidate <= (knob.hardMaximum ?? currentValue) + : candidate < currentValue && candidate >= knob.hardMinimum; + if (!loosens) continue; const visibleComparison = compareOnSlice(knob.ruleId, visible, currentValue, candidate); if (visibleComparison.verdict !== "improved") continue; const heldOutComparison = compareOnSlice(knob.ruleId, heldOut, currentValue, candidate); @@ -268,8 +317,11 @@ export function evaluateKnobDrift( // #8225: a declared tightening ladder joins the pool, so the sentinel's tighter findings and the tighten // apply path judge the SAME candidate values (bounded by the ladder's own hard maximum via declaration). + // #8224: ceiling knobs bound the pool from above (hardMaximum) instead of below. const alternatives = [...new Set([knob.shippedValue, ...knob.candidates, ...(knob.tightening?.candidates ?? [])])] - .filter((value) => value !== liveValue && value >= knob.hardMinimum) + .filter((value) => + value !== liveValue && (knob.orientation === "ceiling" ? value <= (knob.hardMaximum ?? knob.shippedValue) : value >= knob.hardMinimum), + ) .sort((left, right) => Math.abs(left - liveValue) - Math.abs(right - liveValue) || right - left); for (const alternative of alternatives) { @@ -282,7 +334,13 @@ export function evaluateKnobDrift( ruleId: knob.ruleId, liveValue, dominatingValue: alternative, - direction: alternative === knob.shippedValue ? "shipped" : alternative < liveValue ? "looser" : "tighter", + // Orientation decides the label (#8224): for a ceiling knob a HIGHER alternative is the looser one. + direction: + alternative === knob.shippedValue + ? "shipped" + : (knob.orientation === "ceiling" ? alternative > liveValue : alternative < liveValue) + ? "looser" + : "tighter", visibleCases: visible.length, heldOutCases: heldOut.length, visible: visibleComparison, diff --git a/test/unit/knob-loosening-run.test.ts b/test/unit/knob-loosening-run.test.ts index 2d0781a99..816266571 100644 --- a/test/unit/knob-loosening-run.test.ts +++ b/test/unit/knob-loosening-run.test.ts @@ -278,9 +278,14 @@ describe("processor + endpoint wiring (#8176)", () => { expect((await app.request("/v1/internal/calibration/knobs", {}, env)).status).toBe(401); const res = await app.request("/v1/internal/calibration/knobs", { headers: { authorization: `Bearer ${env.INTERNAL_JOB_TOKEN}` } }, env); expect(res.status).toBe(200); - const body = (await res.json()) as { knobs: Array<{ knobId: string; flagEnabled: boolean }> }; - expect(body.knobs.map((knob) => knob.knobId).sort()).toEqual(["ai_review_close_confidence", "satisfaction_floor"]); + const body = (await res.json()) as { knobs: Array<{ knobId: string; flagEnabled: boolean; applyMode: string }> }; + // #8224: report-only knobs list too, labeled by applyMode — the operator sees evidence surfaces + // before any flip-to-live exists. + expect(body.knobs.map((knob) => knob.knobId).sort()).toEqual(["ai_review_close_confidence", "satisfaction_floor", "slop_gate_score"]); expect(body.knobs.every((knob) => knob.flagEnabled === false)).toBe(true); + const byId = Object.fromEntries(body.knobs.map((knob) => [knob.knobId, knob])); + expect(byId.slop_gate_score!.applyMode).toBe("report_only"); + expect(byId.ai_review_close_confidence!.applyMode).toBe("live"); expect(JSON.stringify(body)).not.toMatch(/reward|payout|trust|wallet|hotkey|issueText|modelResponse/i); }); }); diff --git a/test/unit/loosening-knobs.test.ts b/test/unit/loosening-knobs.test.ts index ff33e7e79..224c3c300 100644 --- a/test/unit/loosening-knobs.test.ts +++ b/test/unit/loosening-knobs.test.ts @@ -11,7 +11,7 @@ import { SATISFACTION_FLOOR_SPLIT_SEED, } from "../../src/services/satisfaction-floor-loosening"; import { LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR } from "../../src/services/linked-issue-satisfaction"; -import { DEFAULT_AI_REVIEW_CLOSE_CONFIDENCE } from "../../src/rules/advisory"; +import { DEFAULT_AI_REVIEW_CLOSE_CONFIDENCE, DEFAULT_SLOP_BLOCK_THRESHOLD } from "../../src/rules/advisory"; import { buildKnobReliabilityRecs, buildReportOnlyKnobRecs } from "../../src/review/loosening-recs"; const AI_KNOB = LOOSENABLE_KNOBS.ai_review_close_confidence!; @@ -21,6 +21,7 @@ describe("LOOSENABLE_KNOBS registry invariants (#8159)", () => { expect(LOOSENABLE_KNOBS.satisfaction_floor).toEqual({ knobId: "satisfaction_floor", ruleId: SATISFACTION_FLOOR_RULE_ID, + orientation: "floor", shippedValue: LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR, candidates: SATISFACTION_FLOOR_LOOSENING_CANDIDATES, hardMinimum: SATISFACTION_FLOOR_HARD_MINIMUM, @@ -50,11 +51,25 @@ describe("LOOSENABLE_KNOBS registry invariants (#8159)", () => { const knobs = Object.values(LOOSENABLE_KNOBS); for (const knob of knobs) { expect(knob.candidates.length).toBeGreaterThan(0); - for (const candidate of knob.candidates) { - expect(candidate).toBeLessThan(knob.shippedValue); - expect(candidate).toBeGreaterThanOrEqual(knob.hardMinimum); + // #8224: orientation decides which side of shipped the loosening ladder sits on. + if (knob.orientation === "ceiling") { + expect(typeof knob.hardMaximum).toBe("number"); // a ceiling knob MUST declare its upper bound + for (const candidate of knob.candidates) { + expect(candidate).toBeGreaterThan(knob.shippedValue); + expect(candidate).toBeLessThanOrEqual(knob.hardMaximum!); + } + expect([...knob.candidates].sort((a, b) => a - b)).toEqual([...knob.candidates]); // nearest-first (ascending) + // The live apply path and the tightening ladder are floor-only machinery today — a ceiling knob + // may not go live (or declare a ladder) until their storage/validation generalizes. + expect(knob.applyMode).toBe("report_only"); + expect(knob.tightening).toBeUndefined(); + } else { + for (const candidate of knob.candidates) { + expect(candidate).toBeLessThan(knob.shippedValue); + expect(candidate).toBeGreaterThanOrEqual(knob.hardMinimum); + } + expect([...knob.candidates].sort((a, b) => b - a)).toEqual([...knob.candidates]); // nearest-first (descending) } - expect([...knob.candidates].sort((a, b) => b - a)).toEqual([...knob.candidates]); // nearest-first expect(knob.minVisibleCases).toBeGreaterThan(0); expect(knob.minHeldOutCases).toBeGreaterThan(0); expect(["live", "report_only"]).toContain(knob.applyMode); @@ -377,3 +392,125 @@ describe("evaluateKnobDrift on the close-confidence knob (#8212)", () => { ); }); }); + +// ── #8224: the slop ceiling knob — report-only registration + orientation-aware evaluation ───────────────── + +const SLOP_KNOB = LOOSENABLE_KNOBS.slop_gate_score!; + +function slopCase(targetKey: string, confidence: number, label: "reversed" | "confirmed"): BacktestCase { + return { + ruleId: SLOP_KNOB.ruleId, + targetKey, + outcome: "above_threshold", + label, + firedAt: "2026-06-01T00:00:00.000Z", + decidedAt: "2026-06-02T00:00:00.000Z", + metadata: { confidence }, + }; +} + +const slopProbe = POOL.map((key) => slopCase(key, 0.99, "confirmed")); +const slopSplit = splitBacktestCorpus(slopProbe, SLOP_KNOB.heldOutFraction, SLOP_KNOB.splitSeed); +const slopVisibleKeys = slopSplit.visible.map((c) => c.targetKey); +const slopHeldOutKeys = slopSplit.heldOut.map((c) => c.targetKey); + +// Band blocks at 0.62 the humans REVERSED (the gate over-blocked): shipped 0.60 fires them; raising the +// cap to 0.65 stops firing exactly those — precision up. A deep-high confirmed anchor per slice keeps a +// true positive on both sides of every comparison. +function slopLooseningFriendlyCorpus(): BacktestCase[] { + const cases: BacktestCase[] = []; + for (const key of slopVisibleKeys.slice(0, SLOP_KNOB.minVisibleCases + 6)) cases.push(slopCase(key, 0.62, "reversed")); + for (const key of slopHeldOutKeys.slice(0, SLOP_KNOB.minHeldOutCases + 3)) cases.push(slopCase(key, 0.62, "reversed")); + cases.push(slopCase(slopVisibleKeys[SLOP_KNOB.minVisibleCases + 10]!, 0.9, "confirmed")); + cases.push(slopCase(slopHeldOutKeys[SLOP_KNOB.minHeldOutCases + 6]!, 0.9, "confirmed")); + return cases; +} + +describe("slop_gate_score registry entry (#8224)", () => { + it("pins the ceiling entry: shipped from the gate constant, ascending ladder, report-only, no autonomy plumbing implied", () => { + expect(SLOP_KNOB).toEqual({ + knobId: "slop_gate_score", + ruleId: "slop_gate_score", + orientation: "ceiling", + shippedValue: DEFAULT_SLOP_BLOCK_THRESHOLD / 100, + candidates: [0.65, 0.7], + hardMinimum: DEFAULT_SLOP_BLOCK_THRESHOLD / 100, + hardMaximum: 0.7, + minVisibleCases: 50, + minHeldOutCases: 12, + heldOutFraction: 0.25, + splitSeed: "slop-gate-loosening-v1", + applyMode: "report_only", + overrideFlagKey: "slop_gate_score_override", + looseningEventType: "calibration.slop_gate_score_loosened", + autotuneEnvVar: "SLOP_GATE_SCORE_AUTOTUNE_ENABLED", + }); + expect(LOOSENABLE_KNOBS.quality_gate_score).toBeUndefined(); // stays out until a global shipped default exists + }); + + it("evaluateKnobLoosening proposes the smallest RAISE for a ceiling knob when both splits support it", () => { + const proposal = evaluateKnobLoosening(SLOP_KNOB, slopLooseningFriendlyCorpus()); + expect(proposal).not.toBeNull(); + expect(proposal!.currentValue).toBe(0.6); + expect(proposal!.proposedValue).toBe(0.65); // the smaller of the two raises + expect(proposal!.visible.verdict).toBe("improved"); + expect(proposal!.heldOut.verdict).not.toBe("regressed"); + }); + + it("refuses to raise past the hard maximum, even from an already-raised current value", () => { + expect(evaluateKnobLoosening(SLOP_KNOB, slopLooseningFriendlyCorpus(), SLOP_KNOB.hardMaximum!)).toBeNull(); + }); + + it("never proposes on a sample below the knob's floors", () => { + expect(evaluateKnobLoosening(SLOP_KNOB, slopLooseningFriendlyCorpus().slice(0, 10))).toBeNull(); + }); + + it("drift labels are orientation-aware: an ABOVE-live dominating alternative reads LOOSER for a ceiling knob (and is suppressible)", () => { + const report = evaluateKnobDrift(SLOP_KNOB, slopLooseningFriendlyCorpus()); + expect(report).not.toBeNull(); + expect(report!.dominatingValue).toBe(0.65); + expect(report!.direction).toBe("looser"); // a floor knob would have labeled the same move "tighter" + }); + + it("DEFENSIVE: a ceiling knob missing its hardMaximum can never loosen and its drift pool collapses to at-or-below shipped", () => { + // The structural invariant forbids this shape in the shipped registry; the evaluators still refuse + // rather than treat an absent bound as infinity. + const { hardMaximum: _dropped, ...rest } = SLOP_KNOB; + const unbounded = rest as LoosenableKnob; + expect(evaluateKnobLoosening(unbounded, slopLooseningFriendlyCorpus())).toBeNull(); + const report = evaluateKnobDrift(unbounded, slopLooseningFriendlyCorpus()); + expect(report).toBeNull(); // every above-shipped alternative is filtered out; shipped === live + }); + + it("the generic report-only proposals path picks the slop knob up with zero new plumbing", async () => { + const proposals = await loadReportOnlyKnobProposalsForTest(); + expect(proposals.some((proposal) => proposal.knobId === "slop_gate_score")).toBe(true); + }); +}); + +// Seed a friendly corpus into a real signal store and run the SHIPPED default-registry path — the exact +// call selftune-wire makes each pass. +async function loadReportOnlyKnobProposalsForTest() { + const { createTestEnv } = await import("../helpers/d1"); + const { createSignalStore } = await import("../../src/review/signal-tracking-wire"); + const { loadReportOnlyKnobProposals } = await import("../../src/services/satisfaction-floor-loosening-run"); + const env = createTestEnv(); + const store = createSignalStore(env); + const now = Date.now(); + for (const [i, backtestCase] of slopLooseningFriendlyCorpus().entries()) { + await store.recordRuleFired({ + ruleId: SLOP_KNOB.ruleId, + targetKey: backtestCase.targetKey, + outcome: "above_threshold", + occurredAt: new Date(now - 10_000 - i).toISOString(), + metadata: { confidence: backtestCase.metadata!.confidence as number }, + }); + await store.recordHumanOverride({ + ruleId: SLOP_KNOB.ruleId, + targetKey: backtestCase.targetKey, + verdict: backtestCase.label, + occurredAt: new Date(now - i).toISOString(), + }); + } + return loadReportOnlyKnobProposals(env, now); +}