@@ -24,11 +24,12 @@ import type { LedgerEntry } from "./event-ledger.js";
2424import { MINER_PR_OUTCOME_EVENT } from "./pr-outcome.js" ;
2525import { initPredictionLedger , resolvePredictionLedgerDbPath } from "./prediction-ledger.js" ;
2626import type { PredictionLedgerEntry } from "./prediction-ledger.js" ;
27- import type { PredictedVerdictRecord , ObservedOutcomeRecord , CalibrationReport } from "./calibration-types.js" ;
27+ import type { PredictedVerdictRecord , ObservedOutcomeRecord , CalibrationReport , CalibrationRow } from "./calibration-types.js" ;
2828import { reportCliFailure , describeCliError } from "./cli-error.js" ;
29+ import { runHistoricalReplayCalibrationCycle , type CalibrationSnapshotPayload } from "./calibration-run.js" ;
2930
3031const CALIBRATION_USAGE =
31- "Usage: loopover-miner calibration [--json] | calibration backtest-threshold --candidate <x> [--json] | calibration apply-min-rank --candidate <x> --approve [--json] | calibration revert-min-rank --approve [--json]" ;
32+ "Usage: loopover-miner calibration [--json] | calibration snapshot [--json] | calibration backtest-threshold --candidate <x> [--json] | calibration apply-min-rank --candidate <x> --approve [--json] | calibration revert-min-rank --approve [--json]" ;
3233
3334export type CalibrationCliDeps = {
3435 readFileSync ?: typeof readFileSync ;
@@ -125,6 +126,93 @@ function renderReportText(report: CalibrationReport): void {
125126 }
126127}
127128
129+ /** Map one {@link CalibrationRow} onto the engine's {@link PrOutcomeCalibrationInput} shape (#8317) —
130+ * field-for-field; `hold` is always present on the row so it always forwards. */
131+ export function prOutcomeFromCalibrationRow ( row : CalibrationRow ) : {
132+ mergeConfirmed : number ;
133+ mergeFalse : number ;
134+ closeConfirmed : number ;
135+ closeFalse : number ;
136+ hold : number ;
137+ } {
138+ return {
139+ mergeConfirmed : row . mergeConfirmed ,
140+ mergeFalse : row . mergeFalse ,
141+ closeConfirmed : row . closeConfirmed ,
142+ closeFalse : row . closeFalse ,
143+ hold : row . hold ,
144+ } ;
145+ }
146+
147+ /** `calibration snapshot [--json]` (#8317): run the Phase 7 calibration runner once per project row from
148+ * {@link buildCalibrationReport}, persist a `calibration_snapshot` ledger event per project (with the AMS
149+ * backtest track record attached when any runs exist), and print the results. Mirrors
150+ * {@link runBacktestThreshold}'s open/run/persist/print/finally shape. Does NOT pass a Phase 7 config —
151+ * no `.loopover-ams.yml` calibration-section reader exists yet, so the engine defaults to loop-disabled
152+ * (honest, not a workaround). */
153+ function runCalibrationSnapshot ( args : string [ ] , env : Record < string , string | undefined > , deps : CalibrationCliDeps ) : number {
154+ const json = args . includes ( "--json" ) ;
155+ const unknown = args . find ( ( token ) => token !== "--json" ) ;
156+ if ( unknown ) {
157+ return reportCliFailure ( json , `Unknown option: ${ unknown } . ${ CALIBRATION_USAGE } ` , 1 ) ;
158+ }
159+
160+ let predictionStore ;
161+ let eventLedger ;
162+ try {
163+ predictionStore = initPredictionLedger ( resolvePredictionLedgerDbPath ( env ) ) ;
164+ eventLedger = initEventLedger ( resolveEventLedgerDbPath ( env ) ) ;
165+ const predictionRows = predictionStore . readPredictions ( ) ;
166+ const events = eventLedger . readEvents ( ) ;
167+ const report = buildCalibrationReport ( toPredictionRecords ( predictionRows ) , toOutcomeRecords ( events ) ) ;
168+ const trackRecord = computeAmsBacktestTrackRecord ( readAmsThresholdBacktestRuns ( eventLedger ) ) ;
169+ // #8317: only attach a real history; an empty track record stays null so consumers can tell "no runs yet"
170+ // from "runs exist with zero REGRESSED" without inventing a fabricated zero-run object at the CLI boundary.
171+ const backtestTrackRecord = trackRecord . totalRuns > 0 ? trackRecord : null ;
172+
173+ if ( ! report . hasSignal ) {
174+ const message = "calibration snapshot: no decided predictions yet (predictions need a realized merge/close outcome); nothing persisted." ;
175+ console . log ( json ? JSON . stringify ( { snapshots : [ ] , reason : "no_decided_predictions" } ) : message ) ;
176+ return 0 ;
177+ }
178+
179+ const snapshots : Array < { repoFullName : string ; snapshot : CalibrationSnapshotPayload } > = [ ] ;
180+ for ( const row of report . rows ) {
181+ const cycle = runHistoricalReplayCalibrationCycle (
182+ {
183+ prOutcome : prOutcomeFromCalibrationRow ( row ) ,
184+ repoFullName : row . project ,
185+ backtestTrackRecord,
186+ ...( deps . nowMs !== undefined ? { now : new Date ( deps . nowMs ) . toISOString ( ) } : { } ) ,
187+ } ,
188+ { eventLedger } ,
189+ ) ;
190+ snapshots . push ( { repoFullName : row . project , snapshot : cycle . snapshot } ) ;
191+ }
192+
193+ if ( json ) {
194+ console . log ( JSON . stringify ( { snapshots } , null , 2 ) ) ;
195+ } else {
196+ for ( const entry of snapshots ) {
197+ const accuracy =
198+ entry . snapshot . combinedAccuracy === null ? "n/a" : `${ Math . round ( entry . snapshot . combinedAccuracy * 100 ) } %` ;
199+ console . log (
200+ `calibration snapshot ${ entry . repoFullName } : enabled=${ entry . snapshot . enabled } combined=${ accuracy } ` +
201+ `delta=${ entry . snapshot . deltaFromBaseline === null ? "n/a" : entry . snapshot . deltaFromBaseline . toFixed ( 3 ) } ` +
202+ `sources=${ entry . snapshot . contributingSources . join ( "," ) || "none" } ` ,
203+ ) ;
204+ }
205+ console . log ( `calibration snapshot: persisted ${ snapshots . length } project snapshot(s).` ) ;
206+ }
207+ return 0 ;
208+ } catch ( error ) {
209+ return reportCliFailure ( json , describeCliError ( error ) ) ;
210+ } finally {
211+ predictionStore ?. close ( ) ;
212+ eventLedger ?. close ( ) ;
213+ }
214+ }
215+
128216/** `calibration backtest-threshold --candidate <x>` (#8184): advisory replay of a candidate min-rank skip
129217 * threshold against the taken-opportunity corpus. Prints the shared comparison renderer's report and
130218 * persists the run event. Exit is nonzero ONLY on operational error -- never on verdict (the #8138
@@ -205,13 +293,14 @@ function runMinRankMutation(kind: "apply" | "revert", args: string[], env: Recor
205293}
206294
207295/**
208- * Run `loopover-miner calibration [--json]` (or one of the #8184/#8187 subcommands -- see
296+ * Run `loopover-miner calibration [--json]` (or one of the #8184/#8187/#8317 subcommands -- see
209297 * CALIBRATION_USAGE). The bare form reads the prediction ledger + PR-outcome events, joins them into a
210298 * calibration report, and prints it (a JSON dump under `--json`, else a per-project text summary) along
211299 * with the corpus stats, the backtest track record (#8185), and any current backtest-cleared proposals
212300 * (#8186). Returns the process exit code: 0 on success, 1 on an unknown option.
213301 */
214302export function runCalibrationCli ( args : string [ ] = [ ] , env : Record < string , string | undefined > = process . env , deps : CalibrationCliDeps = { } ) : number {
303+ if ( args [ 0 ] === "snapshot" ) return runCalibrationSnapshot ( args . slice ( 1 ) , env , deps ) ;
215304 if ( args [ 0 ] === "backtest-threshold" ) return runBacktestThreshold ( args . slice ( 1 ) , env , deps ) ;
216305 if ( args [ 0 ] === "apply-min-rank" ) return runMinRankMutation ( "apply" , args . slice ( 1 ) , env , deps ) ;
217306 if ( args [ 0 ] === "revert-min-rank" ) return runMinRankMutation ( "revert" , args . slice ( 1 ) , env , deps ) ;
0 commit comments