diff --git a/packages/gittensory-miner/README.md b/packages/gittensory-miner/README.md index 206bdf70c3..8307bb47f7 100644 --- a/packages/gittensory-miner/README.md +++ b/packages/gittensory-miner/README.md @@ -50,6 +50,14 @@ The package also includes an append-only prediction ledger: `initPredictionLedge codes, plus the producing `ENGINE_VERSION`) in local SQLite, so a later self-improve pass can score predictions against realized outcomes. Insert-only. (#4263) +The package also includes the Phase 7 calibration runner: `runHistoricalReplayCalibrationCycle` +(`lib/calibration-run.js`) scores a completed historical-replay run with the deterministic objective-anchor scorer, +folds the composite into the engine's `computePhase7CalibrationLoop` combine alongside the existing `pr_outcome` +signal, and persists the combined snapshot as a `calibration_snapshot` event — queryable with +`gittensory-miner ledger list --type calibration_snapshot` or `readCalibrationSnapshots` / +`latestCalibrationSnapshot`. It measures and records only; acting on the metric (autonomy bumps, threshold tuning) +stays maintainer-only. See [`docs/miner-selfimprove-calibration.md`](docs/miner-selfimprove-calibration.md). (#4248) + `gittensory-miner manage status` now also folds each tracked repo's current discover/plan/prepare run state (`run-state.js`) alongside its managed PR rows into a "run portfolio" view — `collectRunPortfolio` / `renderRunPortfolioTable` — so a repo actively being discovered or planned shows up even with zero PRs yet. diff --git a/packages/gittensory-miner/docs/miner-selfimprove-calibration.md b/packages/gittensory-miner/docs/miner-selfimprove-calibration.md index 289c120009..6966a9bc49 100644 --- a/packages/gittensory-miner/docs/miner-selfimprove-calibration.md +++ b/packages/gittensory-miner/docs/miner-selfimprove-calibration.md @@ -26,6 +26,27 @@ project, how many predictions were `wouldMerge` vs `wouldClose`, and how each re error)**, and symmetrically for close. `mergePrecision` / `closePrecision` are the headline accuracy numbers a dashboard renders. +## The runner: wiring the replay scorer into the combine (#4248) + +`computePhase7CalibrationLoop` is a **pure combine contract** — by design it cannot schedule replay runs or read +ledgers (the miner depends on the engine, not the reverse), so it waits for an external caller to feed it a real +`historical_replay` composite. #3014 (PR #3225) shipped only that engine side; a 2026-07-08 audit found the closed +issue claimed the two halves were "wired" when in fact **no miner-side runner ever called the replay scorer +(`computeObjectiveAnchor`, #3012) and passed its result into the combine** — two finished pieces, never connected. + +[`lib/calibration-run.js`](../lib/calibration-run.js) (#4248) is that missing runner. +`runHistoricalReplayCalibrationCycle` scores a completed replay run with the deterministic objective-anchor scorer, +reduces the per-task scores to one composite `[0, 1]`, folds it into the `HistoricalReplayCalibrationInput` shape the +engine expects, calls `computePhase7CalibrationLoop` with that **plus the existing `pr_outcome` signal**, and +persists the combined snapshot as a `calibration_snapshot` event on the local append-only event ledger (the same +typed-event-over-`event-ledger.js` pattern as `pr-outcome.js`). The persisted metric is queryable with +`gittensory-miner ledger list --type calibration_snapshot` and via `readCalibrationSnapshots` / +`latestCalibrationSnapshot`. + +Consistent with the boundary below, the runner is **read/measure-only**: it produces and persists the tracked +metric but never acts on it (no autonomy bump, no threshold tune). Acting on the combined accuracy — the +calibration-gated circuit-breaker — remains maintainer-only (#2352). + ## Value-weighting: durable correctness, not volume Raw merge/close precision is not the real objective, and a contributor reading a dashboard number should understand diff --git a/packages/gittensory-miner/lib/calibration-run.d.ts b/packages/gittensory-miner/lib/calibration-run.d.ts new file mode 100644 index 0000000000..f728d37a63 --- /dev/null +++ b/packages/gittensory-miner/lib/calibration-run.d.ts @@ -0,0 +1,161 @@ +import type { + HistoricalReplayCalibrationInput, + Phase7CalibrationConfig, + Phase7CalibrationLoopResult, + Phase7CalibrationManifest, + PrOutcomeCalibrationInput, + ReplayHarnessStatus, +} from "@jsonbored/gittensory-engine"; + +import type { AppendEventInput, LedgerEntry } from "./event-ledger.js"; +import type { + ObjectiveAnchorResult, + ReplayPlanInput, + RevealedHistoryEntry, +} from "./replay-objective-anchor.js"; + +export const MINER_CALIBRATION_SNAPSHOT_EVENT: "calibration_snapshot"; + +/** One completed replay-run task result: what the replay targeted, and the revealed post-T history to score it. */ +export interface ReplayTaskResult { + replayPlan?: ReplayPlanInput | null; + revealedHistory?: RevealedHistoryEntry[] | RevealedHistoryEntry | null; +} + +export interface ScoreCompositeOptions { + computeObjectiveAnchor?: ( + input: { replayPlan?: ReplayPlanInput | null; revealedHistory?: RevealedHistoryEntry[] | RevealedHistoryEntry | null }, + ) => ObjectiveAnchorResult; +} + +export interface HistoricalReplayCompositeScore { + compositeScore: number | null; + sampleSize: number; + scores: number[]; +} + +export function scoreHistoricalReplayComposite( + replayResults: readonly ReplayTaskResult[] | null | undefined, + options?: ScoreCompositeOptions, +): HistoricalReplayCompositeScore; + +/** A completed replay run's descriptor: its per-task results plus the run's identity/freshness/harness health. */ +export interface ReplayRunDescriptor { + replayResults?: readonly ReplayTaskResult[] | null; + replayRunId?: string; + observedAt?: string; + harnessStatus?: ReplayHarnessStatus; +} + +export interface BuiltHistoricalReplayInput { + historicalReplay: HistoricalReplayCalibrationInput | null; + compositeScore: number | null; + sampleSize: number; + scores: number[]; +} + +export function buildHistoricalReplayCalibrationInput( + replayRun: ReplayRunDescriptor | null | undefined, + options?: ScoreCompositeOptions, +): BuiltHistoricalReplayInput; + +/** The persisted, public-safe projection of a Phase7CalibrationLoopResult. */ +export interface CalibrationSnapshotPayload { + enabled: boolean; + combinedAccuracy: number | null; + baselineAccuracy: number; + deltaFromBaseline: number | null; + autonomyIncreasePermitted: boolean; + replayHarnessHold: boolean; + replayHarnessStatus: string; + replayRunDue: boolean; + holdReasons: string[]; + contributingSources: string[]; + replayRunId: string | null; + observedAt: string | null; + replaySampleSize: number; +} + +export interface SnapshotMeta { + replayRunId?: string | null; + observedAt?: string | null; + sampleSize?: number; +} + +export function snapshotPayloadFromResult( + result: Phase7CalibrationLoopResult, + meta?: SnapshotMeta, +): CalibrationSnapshotPayload; + +export function normalizeCalibrationSnapshotPayload(payload: unknown): CalibrationSnapshotPayload | null; + +export interface RecordCalibrationSnapshotOptions { + /** Optional at the type level so a caller can pass an unusable ledger to exercise the fail-closed guard; the + * writer throws `invalid_event_ledger` at runtime when this is absent or lacks `appendEvent`. */ + eventLedger?: { appendEvent(event: AppendEventInput): LedgerEntry }; + repoFullName?: string; +} + +export function recordCalibrationSnapshot( + input: unknown, + options?: RecordCalibrationSnapshotOptions, +): LedgerEntry | null; + +export interface CalibrationSnapshotReader { + readEvents(filter?: { since?: number | null; repoFullName?: string | null }): unknown[]; +} + +export interface CalibrationSnapshotFilter { + since?: number | null; + repoFullName?: string | null; +} + +export interface PersistedCalibrationSnapshot extends CalibrationSnapshotPayload { + repoFullName: string | null; + seq: number | null; + createdAt: string | null; +} + +export function readCalibrationSnapshots( + eventLedger: CalibrationSnapshotReader, + filter?: CalibrationSnapshotFilter, +): PersistedCalibrationSnapshot[]; + +export function latestCalibrationSnapshot( + eventLedger: CalibrationSnapshotReader, + filter?: CalibrationSnapshotFilter, +): PersistedCalibrationSnapshot | null; + +export interface RunCalibrationCycleInput { + config?: Phase7CalibrationConfig | Phase7CalibrationManifest | Record | null; + prOutcome?: PrOutcomeCalibrationInput | null; + replayRun?: ReplayRunDescriptor | null; + now?: string | Date | null; + observedAt?: string | null; + repoFullName?: string; +} + +export interface RunCalibrationCycleDeps extends ScoreCompositeOptions { + computeLoop?: (input: { + config?: Phase7CalibrationConfig | Phase7CalibrationManifest | Record | null; + prOutcome?: PrOutcomeCalibrationInput | null; + historicalReplay?: HistoricalReplayCalibrationInput | null; + now?: string | Date | null; + }) => Phase7CalibrationLoopResult; + eventLedger?: { appendEvent(event: AppendEventInput): LedgerEntry }; +} + +export interface RunCalibrationCycleResult { + result: Phase7CalibrationLoopResult; + snapshot: CalibrationSnapshotPayload; + recorded: LedgerEntry | null; + historicalReplay: HistoricalReplayCalibrationInput | null; + compositeScore: number | null; + sampleSize: number; + scores: number[]; +} + +export function runHistoricalReplayCalibrationCycle( + input?: RunCalibrationCycleInput, + deps?: RunCalibrationCycleDeps, +): RunCalibrationCycleResult; diff --git a/packages/gittensory-miner/lib/calibration-run.js b/packages/gittensory-miner/lib/calibration-run.js new file mode 100644 index 0000000000..ac6fc2182a --- /dev/null +++ b/packages/gittensory-miner/lib/calibration-run.js @@ -0,0 +1,232 @@ +// Phase 7 calibration runner (#4248): the miner-side runner that finally CONNECTS the two finished-but-unwired +// halves #3014 left apart. #3014 landed the engine's pure calibration *combine* contract +// (`computePhase7CalibrationLoop`, packages/gittensory-engine/src/phase7-calibration-loop.ts) and #3012 landed the +// deterministic replay *scorer* (`computeObjectiveAnchor`, ./replay-objective-anchor.js), but nothing ever called +// one with the other -- #3014's issue claimed "wired" while only the engine side shipped. This module is the +// missing runner: it scores a completed historical-replay run with the objective-anchor scorer, folds the +// resulting composite into the `HistoricalReplayCalibrationInput` shape the engine expects, calls the combine with +// the existing pr_outcome signal, and PERSISTS the combined snapshot to the local append-only event ledger (a typed +// event layered on event-ledger.js exactly like pr-outcome.js's MINER_PR_OUTCOME_EVENT), queryable via +// `gittensory-miner ledger list --type calibration_snapshot`. +// +// SCOPE: this runner is read/measure-only. It produces and persists the tracked calibration metric; it NEVER acts +// on it (no autonomy-level bump, no gate-threshold tune) -- that enforcement is maintainer-only and fail-closed +// (see docs/miner-selfimprove-calibration.md's maintainer-only boundary). The engine owns the deterministic +// combine/freshness/threshold/hold-reason logic; this module owns scheduling the score and persisting the row. + +import { computePhase7CalibrationLoop } from "@jsonbored/gittensory-engine"; +import { computeObjectiveAnchor } from "./replay-objective-anchor.js"; + +/** Event-ledger vocabulary for a persisted Phase 7 calibration snapshot (mirrors MINER_PR_OUTCOME_EVENT). */ +export const MINER_CALIBRATION_SNAPSHOT_EVENT = "calibration_snapshot"; + +const SCORE_PRECISION = 1e6; + +function roundScore(value) { + return Math.round(Math.min(1, Math.max(0, value)) * SCORE_PRECISION) / SCORE_PRECISION; +} + +function isFiniteNumber(value) { + return typeof value === "number" && Number.isFinite(value); +} + +function numberOrNull(value) { + return isFiniteNumber(value) ? value : null; +} + +function optionalString(value) { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed || null; +} + +/** + * Score a completed replay run's per-task results with the deterministic objective-anchor scorer and reduce them to + * one composite `[0, 1]` accuracy (the mean of the per-task scores). `replayResults` is a list of + * `{ replayPlan, revealedHistory }` pairs; each non-object entry is defensively skipped. Returns `compositeScore: + * null` (never a fabricated 0) when there is no scorable task. Pure aside from the injected scorer. + */ +export function scoreHistoricalReplayComposite(replayResults, options = {}) { + const scoreOne = options.computeObjectiveAnchor ?? computeObjectiveAnchor; + const list = Array.isArray(replayResults) ? replayResults : []; + const scores = []; + for (const entry of list) { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue; + const { score } = scoreOne({ replayPlan: entry.replayPlan, revealedHistory: entry.revealedHistory }); + if (isFiniteNumber(score)) scores.push(score); + } + const sampleSize = scores.length; + const compositeScore = sampleSize === 0 ? null : roundScore(scores.reduce((sum, s) => sum + s, 0) / sampleSize); + return { compositeScore, sampleSize, scores }; +} + +/** + * Build the engine's `HistoricalReplayCalibrationInput` from a replay run descriptor + * (`{ replayResults, replayRunId, observedAt, harnessStatus }`). Returns `historicalReplay: null` when no run + * descriptor is supplied (the engine then holds `no_historical_replay_signal` when the loop is enabled). When a run + * IS supplied its `harnessStatus` flows through verbatim so a degraded/unavailable harness still reaches the + * engine's fail-closed hold path even if it scored zero tasks; a null composite becomes `0` only for the engine's + * numeric contract (the un-fabricated `compositeScore`/`sampleSize` are returned alongside for the snapshot). + */ +export function buildHistoricalReplayCalibrationInput(replayRun, options = {}) { + if (!replayRun || typeof replayRun !== "object" || Array.isArray(replayRun)) { + return { historicalReplay: null, compositeScore: null, sampleSize: 0, scores: [] }; + } + const composite = scoreHistoricalReplayComposite(replayRun.replayResults, options); + return { + historicalReplay: { + compositeScore: composite.compositeScore ?? 0, + replayRunId: replayRun.replayRunId, + observedAt: replayRun.observedAt, + harnessStatus: replayRun.harnessStatus, + }, + compositeScore: composite.compositeScore, + sampleSize: composite.sampleSize, + scores: composite.scores, + }; +} + +/** + * Derive a JSON-safe, public-safe snapshot payload from a computed `Phase7CalibrationLoopResult`. Only accuracies, + * the documented baseline, hold-reason CODES, and provenance are surfaced -- never raw replay scores or rewards. + * Every field is a number/null, boolean, string/null, or string[] so it round-trips through the event ledger's + * verbatim-JSON serializer unchanged. + */ +export function snapshotPayloadFromResult(result, meta = {}) { + return { + enabled: result.enabled === true, + combinedAccuracy: numberOrNull(result.combinedAccuracy), + baselineAccuracy: isFiniteNumber(result.baselineAccuracy) ? result.baselineAccuracy : 0, + deltaFromBaseline: numberOrNull(result.deltaFromBaseline), + autonomyIncreasePermitted: result.autonomyIncreasePermitted === true, + replayHarnessHold: result.replayHarnessHold === true, + replayHarnessStatus: optionalString(result.replayHarnessStatus) ?? "missing", + replayRunDue: result.replayRunDue === true, + holdReasons: Array.isArray(result.holdReasons) ? result.holdReasons.map(String) : [], + contributingSources: Array.isArray(result.audit?.contributingSources) + ? result.audit.contributingSources.map(String) + : [], + replayRunId: optionalString(meta.replayRunId), + observedAt: optionalString(meta.observedAt), + replaySampleSize: Number.isInteger(meta.sampleSize) && meta.sampleSize >= 0 ? meta.sampleSize : 0, + }; +} + +/** + * Validate + normalize a calibration-snapshot payload, returning `null` on any malformed shape (mirrors + * pr-outcome.js's `normalizePrOutcomePayload`, so a corrupted row can neither be written nor read back). Skipped + * rows are dropped by the reader rather than throwing. + */ +export function normalizeCalibrationSnapshotPayload(payload) { + if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null; + if (payload.combinedAccuracy !== null && !isFiniteNumber(payload.combinedAccuracy)) return null; + if (!isFiniteNumber(payload.baselineAccuracy)) return null; + if (payload.deltaFromBaseline !== null && !isFiniteNumber(payload.deltaFromBaseline)) return null; + if (typeof payload.autonomyIncreasePermitted !== "boolean") return null; + const replayHarnessStatus = optionalString(payload.replayHarnessStatus); + if (!replayHarnessStatus) return null; + if (!Array.isArray(payload.holdReasons) || payload.holdReasons.some((code) => typeof code !== "string")) { + return null; + } + const contributingSources = Array.isArray(payload.contributingSources) + ? payload.contributingSources.filter((code) => typeof code === "string") + : []; + return { + enabled: payload.enabled === true, + combinedAccuracy: payload.combinedAccuracy, + baselineAccuracy: payload.baselineAccuracy, + deltaFromBaseline: payload.deltaFromBaseline, + autonomyIncreasePermitted: payload.autonomyIncreasePermitted, + replayHarnessHold: payload.replayHarnessHold === true, + replayHarnessStatus, + replayRunDue: payload.replayRunDue === true, + holdReasons: payload.holdReasons, + contributingSources, + replayRunId: optionalString(payload.replayRunId), + observedAt: optionalString(payload.observedAt), + replaySampleSize: + Number.isInteger(payload.replaySampleSize) && payload.replaySampleSize >= 0 ? payload.replaySampleSize : 0, + }; +} + +/** + * Persist one calibration snapshot to an INJECTED event ledger (same dependency-injection shape as pr-outcome.js's + * `recordPrOutcomeSnapshot`, so it's unit-testable without a real SQLite file). Fail-soft: a malformed payload + * returns `null` without appending. An unusable ledger is the only hard error (a programmer wiring mistake). + */ +export function recordCalibrationSnapshot(input, options = {}) { + const eventLedger = options.eventLedger; + if (!eventLedger || typeof eventLedger.appendEvent !== "function") throw new Error("invalid_event_ledger"); + const payload = normalizeCalibrationSnapshotPayload(input); + if (!payload) return null; + const repoFullName = optionalString(options.repoFullName); + return eventLedger.appendEvent({ + type: MINER_CALIBRATION_SNAPSHOT_EVENT, + ...(repoFullName ? { repoFullName } : {}), + payload, + }); +} + +/** + * Read every persisted calibration snapshot from the injected ledger's ascending append-only stream (mirrors + * pr-outcome.js's `readPrOutcomes`). Foreign event types and malformed payloads are skipped; a ledger that cannot + * read reduces to an empty list. Returns snapshots in ledger order (oldest first). + */ +export function readCalibrationSnapshots(eventLedger, filter = {}) { + const events = + eventLedger && typeof eventLedger.readEvents === "function" ? eventLedger.readEvents(filter) : []; + const snapshots = []; + for (const event of Array.isArray(events) ? events : []) { + if (event?.type !== MINER_CALIBRATION_SNAPSHOT_EVENT) continue; + const normalized = normalizeCalibrationSnapshotPayload(event.payload); + if (!normalized) continue; + snapshots.push({ + ...normalized, + repoFullName: typeof event.repoFullName === "string" ? event.repoFullName : null, + seq: Number.isInteger(event.seq) ? event.seq : null, + createdAt: optionalString(event.createdAt), + }); + } + return snapshots; +} + +/** The most recent persisted calibration snapshot, or `null` when none exist. */ +export function latestCalibrationSnapshot(eventLedger, filter = {}) { + const snapshots = readCalibrationSnapshots(eventLedger, filter); + return snapshots.length > 0 ? snapshots[snapshots.length - 1] : null; +} + +/** + * The runner. Scores the replay run (via the objective-anchor scorer), calls the engine's calibration combine with + * the resulting historical-replay composite plus the existing pr_outcome signal, and -- when an event ledger is + * injected -- persists the combined snapshot. Returns the engine result, the derived snapshot payload, the recorded + * ledger entry (or null when no ledger was injected or the payload was malformed), and the un-fabricated + * composite/sample provenance. The engine combine (`computeLoop`) is injectable so unit tests can pin it. + */ +export function runHistoricalReplayCalibrationCycle(input = {}, deps = {}) { + const computeLoop = deps.computeLoop ?? computePhase7CalibrationLoop; + const built = buildHistoricalReplayCalibrationInput(input.replayRun, deps); + const result = computeLoop({ + config: input.config, + prOutcome: input.prOutcome, + historicalReplay: built.historicalReplay, + now: input.now, + }); + const snapshot = snapshotPayloadFromResult(result, { + replayRunId: built.historicalReplay?.replayRunId ?? null, + observedAt: input.observedAt ?? built.historicalReplay?.observedAt ?? null, + sampleSize: built.sampleSize, + }); + const recorded = deps.eventLedger + ? recordCalibrationSnapshot(snapshot, { eventLedger: deps.eventLedger, repoFullName: input.repoFullName }) + : null; + return { + result, + snapshot, + recorded, + historicalReplay: built.historicalReplay, + compositeScore: built.compositeScore, + sampleSize: built.sampleSize, + scores: built.scores, + }; +} diff --git a/packages/gittensory-miner/package.json b/packages/gittensory-miner/package.json index bb935d5006..9b36ed41ef 100644 --- a/packages/gittensory-miner/package.json +++ b/packages/gittensory-miner/package.json @@ -33,7 +33,7 @@ "expected-engine.version" ], "scripts": { - "build": "node --check bin/gittensory-miner.js && node --check bin/gittensory-miner-mcp.js && node --check lib/ams-policy.js && node --check lib/attempt-cli.js && node --check lib/attempt-input-builder.js && node --check lib/attempt-log.js && node --check lib/attempt-runner.js && node --check lib/attempt-worktree.js && node --check lib/calibration-types.js && node --check lib/calibration.js && node --check lib/ci-poller.js && node --check lib/claim-adjudication.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger.js && node --check lib/cli.js && node --check lib/coding-agent-construction.js && node --check lib/coding-agent-house-rules.js && node --check lib/coding-task-spec.js && node --check lib/deny-check.js && node --check lib/deny-hook-synthesis.js && node --check lib/deny-hooks.js && node --check lib/deployment-docs-audit.js && node --check lib/discover-cli.js && node --check lib/event-ledger-cli.js && node --check lib/event-ledger.js && node --check lib/execute-local-write.js && node --check lib/feasibility-cli.js && node --check lib/gate-verdict-poller.js && node --check lib/governor-action-mode.js && node --check lib/governor-chokepoint-persisted.js && node --check lib/governor-chokepoint.js && node --check lib/governor-kill-switch.js && node --check lib/governor-ledger-cli.js && node --check lib/governor-ledger.js && node --check lib/governor-open-pr.js && node --check lib/governor-run-halt.js && node --check lib/governor-state.js && node --check lib/governor-write-rate-limit.js && node --check lib/harness-submission-trigger.js && node --check lib/laptop-init.js && node --check lib/live-issue-snapshot.js && node --check lib/local-store.js && node --check lib/loop-cli.js && node --check lib/loop-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.js && node --check lib/miner-goal-spec.js && node --check lib/opportunity-fanout.js && node --check lib/opportunity-ranker.js && node --check lib/orb-export.js && node --check lib/plan-store-cli.js && node --check lib/plan-store.js && node --check lib/portfolio-dashboard.js && node --check lib/portfolio-discovery.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-expiry.js && node --check lib/pr-disposition-poller.js && node --check lib/pr-outcome.js && node --check lib/prediction-ledger.js && node --check lib/pretooluse-hook.js && node --check lib/rejection-signal.js && node --check lib/rejection-state-machine.js && node --check lib/rejection-templates.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-snapshot.js && node --check lib/replay-task-generation.js && node --check lib/repo-clone.js && node --check lib/run-state-cli.js && node --check lib/run-state.js && node --check lib/self-review-context.js && node --check lib/slop-assessment.js && node --check lib/status.js && node --check lib/submission-freshness-check.js && node --check lib/update-check.js && node --check lib/version.js && node --check lib/worktree-allocator.js" + "build": "node --check bin/gittensory-miner.js && node --check bin/gittensory-miner-mcp.js && node --check lib/ams-policy.js && node --check lib/attempt-cli.js && node --check lib/attempt-input-builder.js && node --check lib/attempt-log.js && node --check lib/attempt-runner.js && node --check lib/attempt-worktree.js && node --check lib/calibration-run.js && node --check lib/calibration-types.js && node --check lib/calibration.js && node --check lib/ci-poller.js && node --check lib/claim-adjudication.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger.js && node --check lib/cli.js && node --check lib/coding-agent-construction.js && node --check lib/coding-agent-house-rules.js && node --check lib/coding-task-spec.js && node --check lib/deny-check.js && node --check lib/deny-hook-synthesis.js && node --check lib/deny-hooks.js && node --check lib/deployment-docs-audit.js && node --check lib/discover-cli.js && node --check lib/event-ledger-cli.js && node --check lib/event-ledger.js && node --check lib/execute-local-write.js && node --check lib/feasibility-cli.js && node --check lib/gate-verdict-poller.js && node --check lib/governor-action-mode.js && node --check lib/governor-chokepoint-persisted.js && node --check lib/governor-chokepoint.js && node --check lib/governor-kill-switch.js && node --check lib/governor-ledger-cli.js && node --check lib/governor-ledger.js && node --check lib/governor-open-pr.js && node --check lib/governor-run-halt.js && node --check lib/governor-state.js && node --check lib/governor-write-rate-limit.js && node --check lib/harness-submission-trigger.js && node --check lib/laptop-init.js && node --check lib/live-issue-snapshot.js && node --check lib/local-store.js && node --check lib/loop-cli.js && node --check lib/loop-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.js && node --check lib/miner-goal-spec.js && node --check lib/opportunity-fanout.js && node --check lib/opportunity-ranker.js && node --check lib/orb-export.js && node --check lib/plan-store-cli.js && node --check lib/plan-store.js && node --check lib/portfolio-dashboard.js && node --check lib/portfolio-discovery.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-expiry.js && node --check lib/pr-disposition-poller.js && node --check lib/pr-outcome.js && node --check lib/prediction-ledger.js && node --check lib/pretooluse-hook.js && node --check lib/rejection-signal.js && node --check lib/rejection-state-machine.js && node --check lib/rejection-templates.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-snapshot.js && node --check lib/replay-task-generation.js && node --check lib/repo-clone.js && node --check lib/run-state-cli.js && node --check lib/run-state.js && node --check lib/self-review-context.js && node --check lib/slop-assessment.js && node --check lib/status.js && node --check lib/submission-freshness-check.js && node --check lib/update-check.js && node --check lib/version.js && node --check lib/worktree-allocator.js" }, "dependencies": { "@jsonbored/gittensory-engine": "*", diff --git a/test/integration/miner-calibration-loop.test.ts b/test/integration/miner-calibration-loop.test.ts new file mode 100644 index 0000000000..ddd5a8b5f7 --- /dev/null +++ b/test/integration/miner-calibration-loop.test.ts @@ -0,0 +1,142 @@ +/** + * End-to-end regression for the Phase 7 calibration wiring (#4248): the full chain the closed #3014 claimed but + * never connected — the deterministic replay scorer (replay-objective-anchor.js, #3012) → the engine's combine + * contract (computePhase7CalibrationLoop, #3014) → a persisted, queryable ledger row. Per-module edge cases stay in + * test/unit/miner-calibration-run.test.ts and packages/gittensory-engine/test/phase7-calibration-loop.test.ts; this + * file pins the composed chain against a REAL engine combine and a REAL temp-file event ledger. + */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +// Resolve the engine PACKAGE import inside calibration-run.js to the in-repo source, so the runner's default +// (non-injected) combine runs the real computePhase7CalibrationLoop rather than a stub. +vi.mock("@jsonbored/gittensory-engine", async () => import("../../packages/gittensory-engine/src/index")); + +import { + MINER_CALIBRATION_SNAPSHOT_EVENT, + readCalibrationSnapshots, + runHistoricalReplayCalibrationCycle, +} from "../../packages/gittensory-miner/lib/calibration-run.js"; +import { filterLedgerEvents, runLedgerList } from "../../packages/gittensory-miner/lib/event-ledger-cli.js"; +import { initEventLedger } from "../../packages/gittensory-miner/lib/event-ledger.js"; + +const roots: string[] = []; + +function tempLedgerPath() { + const root = mkdtempSync(join(tmpdir(), "gittensory-miner-calibration-")); + roots.push(root); + return join(root, "event-ledger.sqlite3"); +} + +afterEach(() => { + vi.restoreAllMocks(); + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +describe("miner Phase 7 calibration loop (#4248)", () => { + it("scores a replay run, combines it with pr_outcome in the real engine, and persists a queryable snapshot", () => { + const ledger = initEventLedger(tempLedgerPath()); + try { + const out = runHistoricalReplayCalibrationCycle( + { + config: { miner: { calibration: { phase7LoopEnabled: true, prOutcomeMinDecided: 1 } } }, + // pr_outcome confusion matrix → 8/10 correct → 0.8 accuracy. + prOutcome: { mergeConfirmed: 8, mergeFalse: 2, closeConfirmed: 0, closeFalse: 0 }, + replayRun: { + // A fully-overlapping feature task → objective-anchor score 1.0. + replayResults: [ + { + replayPlan: { pathsTouched: ["src/x/a.ts"], title: "feat: cache invalidation" }, + revealedHistory: [{ pathsTouched: ["src/x/b.ts"], title: "feat: cache fix" }], + }, + ], + replayRunId: "replay-run-2026-07-09", + observedAt: "2026-07-09T00:00:00.000Z", + harnessStatus: "healthy", + }, + now: "2026-07-10T00:00:00.000Z", + repoFullName: "acme/widgets", + }, + { eventLedger: ledger }, + ); + + // The historical_replay accuracy came from the SCORER (1.0), not a hardcoded value, and combined 0.5/0.5 with + // the 0.8 pr_outcome signal → 0.9, clearing the default 0.70 autonomy threshold with both sources present. + expect(out.result.bySource.historical_replay.accuracy).toBe(1); + expect(out.result.bySource.historical_replay.replayRunId).toBe("replay-run-2026-07-09"); + expect(out.result.bySource.pr_outcome.accuracy).toBe(0.8); + expect(out.result.combinedAccuracy).toBe(0.9); + expect(out.result.autonomyIncreasePermitted).toBe(true); + expect(out.recorded).not.toBeNull(); + + // Persisted row is readable back through the typed reader from a FRESH connection to the same file. + const reopened = initEventLedger(ledger.dbPath); + try { + const snapshots = readCalibrationSnapshots(reopened, { repoFullName: "acme/widgets" }); + expect(snapshots).toHaveLength(1); + expect(snapshots[0]).toMatchObject({ + combinedAccuracy: 0.9, + autonomyIncreasePermitted: true, + replayHarnessStatus: "healthy", + replayRunId: "replay-run-2026-07-09", + replaySampleSize: 1, + repoFullName: "acme/widgets", + }); + + // Deliverable: the snapshot is queryable via the EXISTING `gittensory-miner ledger list --type` tooling. + const typed = filterLedgerEvents(reopened.readEvents(), { type: MINER_CALIBRATION_SNAPSHOT_EVENT }); + expect(typed).toHaveLength(1); + + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const exitCode = runLedgerList(["--type", MINER_CALIBRATION_SNAPSHOT_EVENT, "--json"], { + initEventLedger: () => reopened, + }); + expect(exitCode).toBe(0); + const printed = JSON.parse(logSpy.mock.calls[0]?.[0] as string) as { events: Array<{ type: string }> }; + expect(printed.events).toHaveLength(1); + expect(printed.events[0]?.type).toBe(MINER_CALIBRATION_SNAPSHOT_EVENT); + } finally { + reopened.close(); + } + } finally { + ledger.close(); + } + }); + + it("holds fail-closed and still persists the snapshot when the replay harness is degraded", () => { + const ledger = initEventLedger(tempLedgerPath()); + try { + const out = runHistoricalReplayCalibrationCycle( + { + config: { miner: { calibration: { phase7LoopEnabled: true, prOutcomeMinDecided: 1 } } }, + prOutcome: { mergeConfirmed: 9, mergeFalse: 1, closeConfirmed: 0, closeFalse: 0 }, + replayRun: { + replayResults: [ + { + replayPlan: { pathsTouched: ["src/x/a.ts"], title: "feat: x" }, + revealedHistory: [{ pathsTouched: ["src/x/b.ts"], title: "feat: y" }], + }, + ], + replayRunId: "replay-degraded", + observedAt: "2026-07-09T00:00:00.000Z", + harnessStatus: "degraded", + }, + now: "2026-07-10T00:00:00.000Z", + }, + { eventLedger: ledger }, + ); + + // A degraded harness is a fail-closed hold: no autonomy increase, and historical_replay is rejected even + // though pr_outcome had signal — so the combined metric never contributes to an increase. + expect(out.result.autonomyIncreasePermitted).toBe(false); + expect(out.result.replayHarnessHold).toBe(true); + expect(out.snapshot.replayHarnessStatus).toBe("degraded"); + expect(out.recorded).not.toBeNull(); + expect(readCalibrationSnapshots(ledger)).toHaveLength(1); + } finally { + ledger.close(); + } + }); +}); diff --git a/test/unit/miner-calibration-run.test.ts b/test/unit/miner-calibration-run.test.ts new file mode 100644 index 0000000000..92ef0be113 --- /dev/null +++ b/test/unit/miner-calibration-run.test.ts @@ -0,0 +1,398 @@ +import { describe, expect, it, vi } from "vitest"; + +// The runner imports `computePhase7CalibrationLoop` from the engine PACKAGE; resolve it to the in-repo source so the +// default (non-injected) combine branch runs against the real engine, exactly like miner-feasibility-cli.test.ts. +vi.mock("@jsonbored/gittensory-engine", async () => import("../../packages/gittensory-engine/src/index")); + +import { + MINER_CALIBRATION_SNAPSHOT_EVENT, + buildHistoricalReplayCalibrationInput, + latestCalibrationSnapshot, + normalizeCalibrationSnapshotPayload, + readCalibrationSnapshots, + recordCalibrationSnapshot, + runHistoricalReplayCalibrationCycle, + scoreHistoricalReplayComposite, + snapshotPayloadFromResult, +} from "../../packages/gittensory-miner/lib/calibration-run.js"; +import type { AppendEventInput, LedgerEntry } from "../../packages/gittensory-miner/lib/event-ledger.js"; + +// A minimal injected event ledger (the DI shape record/read accept) — pure unit tests, no SQLite file. `_events` is +// exposed so a test can inject crafted rows for the reader's defensive skip branches. Typed against the real +// EventLedger#appendEvent contract so this mock can't silently drift from it. +function mockLedger(): { + appendEvent: (e: AppendEventInput) => LedgerEntry; + readEvents: (filter?: { repoFullName?: string }) => unknown[]; + _events: Array>; +} { + const events: Array> = []; + let seq = 0; + return { + appendEvent: (e) => { + const entry = { + id: ++seq, + seq, + type: e.type, + repoFullName: e.repoFullName ?? null, + payload: e.payload, + createdAt: new Date().toISOString(), + }; + events.push(entry); + return entry as unknown as LedgerEntry; + }, + readEvents: (filter = {}) => + events.filter((e) => filter.repoFullName === undefined || e.repoFullName === filter.repoFullName), + _events: events, + }; +} + +/** A complete, valid snapshot payload; individual tests override single fields to exercise each reject branch. */ +function validPayload(overrides: Record = {}): Record { + return { + enabled: true, + combinedAccuracy: 0.9, + baselineAccuracy: 0.62, + deltaFromBaseline: 0.28, + autonomyIncreasePermitted: true, + replayHarnessHold: false, + replayHarnessStatus: "healthy", + replayRunDue: false, + holdReasons: [], + contributingSources: ["historical_replay", "pr_outcome"], + replayRunId: "run-1", + observedAt: "2026-07-09T00:00:00.000Z", + replaySampleSize: 3, + ...overrides, + }; +} + +/** A minimal Phase7CalibrationLoopResult-shaped object for snapshotPayloadFromResult branch tests. */ +function fakeResult(overrides: Record = {}): Record { + return { + enabled: true, + combinedAccuracy: 0.9, + baselineAccuracy: 0.62, + deltaFromBaseline: 0.28, + autonomyIncreasePermitted: true, + replayHarnessHold: false, + replayHarnessStatus: "healthy", + replayRunDue: false, + holdReasons: ["calibration_below_threshold"], + audit: { contributingSources: ["pr_outcome"], rejectedSources: [] }, + ...overrides, + }; +} + +const FEAT_REPLAY = { replayPlan: { pathsTouched: ["src/x/a.ts"], title: "feat: x" }, revealedHistory: [{ pathsTouched: ["src/x/b.ts"], title: "feat: y" }] }; + +describe("scoreHistoricalReplayComposite (#4248)", () => { + it("scores each task with the real objective-anchor scorer and returns the mean composite", () => { + // Two identical fully-overlapping feature tasks → each scores 1.0 → composite 1.0. + const out = scoreHistoricalReplayComposite([FEAT_REPLAY, FEAT_REPLAY]); + expect(out.sampleSize).toBe(2); + expect(out.scores).toEqual([1, 1]); + expect(out.compositeScore).toBe(1); + }); + + it("returns a null composite (never a fabricated 0) and zero samples for a non-array or empty input", () => { + expect(scoreHistoricalReplayComposite(null)).toEqual({ compositeScore: null, sampleSize: 0, scores: [] }); + expect(scoreHistoricalReplayComposite([])).toEqual({ compositeScore: null, sampleSize: 0, scores: [] }); + }); + + it("skips non-object entries and any scorer result whose score is not finite (injected scorer)", () => { + let call = 0; + const out = scoreHistoricalReplayComposite( + [null, [1], "x", { replayPlan: {} }, { replayPlan: {} }] as never, + { + // First kept entry scores NaN (dropped), second scores 0.5 (kept). + computeObjectiveAnchor: () => { + call += 1; + return { score: call === 1 ? Number.NaN : 0.5 } as never; + }, + }, + ); + expect(call).toBe(2); // only the two object entries reached the scorer + expect(out.scores).toEqual([0.5]); + expect(out.compositeScore).toBe(0.5); + expect(out.sampleSize).toBe(1); + }); +}); + +describe("buildHistoricalReplayCalibrationInput (#4248)", () => { + it("returns a null historicalReplay for an absent / non-object / array run descriptor", () => { + for (const bad of [null, undefined, "x", [FEAT_REPLAY]]) { + const built = buildHistoricalReplayCalibrationInput(bad as never); + expect(built).toEqual({ historicalReplay: null, compositeScore: null, sampleSize: 0, scores: [] }); + } + }); + + it("folds the composite into the engine input shape, passing harness metadata through verbatim", () => { + const built = buildHistoricalReplayCalibrationInput({ + replayResults: [FEAT_REPLAY], + replayRunId: "run-9", + observedAt: "2026-07-09T00:00:00.000Z", + harnessStatus: "healthy", + }); + expect(built.compositeScore).toBe(1); + expect(built.sampleSize).toBe(1); + expect(built.historicalReplay).toEqual({ + compositeScore: 1, + replayRunId: "run-9", + observedAt: "2026-07-09T00:00:00.000Z", + harnessStatus: "healthy", + }); + }); + + it("coerces a null composite (no scorable task) to a 0 for the engine's numeric contract", () => { + const built = buildHistoricalReplayCalibrationInput({ replayResults: [], harnessStatus: "degraded" }); + expect(built.compositeScore).toBeNull(); + expect(built.sampleSize).toBe(0); + expect(built.historicalReplay?.compositeScore).toBe(0); + expect(built.historicalReplay?.harnessStatus).toBe("degraded"); + }); +}); + +describe("snapshotPayloadFromResult (#4248)", () => { + it("projects a full result to a JSON-safe public-safe payload", () => { + const payload = snapshotPayloadFromResult(fakeResult() as never, { + replayRunId: "run-1", + observedAt: "2026-07-09T00:00:00.000Z", + sampleSize: 4, + }); + expect(payload).toEqual({ + enabled: true, + combinedAccuracy: 0.9, + baselineAccuracy: 0.62, + deltaFromBaseline: 0.28, + autonomyIncreasePermitted: true, + replayHarnessHold: false, + replayHarnessStatus: "healthy", + replayRunDue: false, + holdReasons: ["calibration_below_threshold"], + contributingSources: ["pr_outcome"], + replayRunId: "run-1", + observedAt: "2026-07-09T00:00:00.000Z", + replaySampleSize: 4, + }); + }); + + it("falls back safely on every absent/degenerate field (the false side of each guard)", () => { + const payload = snapshotPayloadFromResult( + { + enabled: false, + combinedAccuracy: null, + baselineAccuracy: Number.NaN, + deltaFromBaseline: null, + autonomyIncreasePermitted: false, + replayHarnessHold: false, + replayHarnessStatus: 42, + replayRunDue: false, + holdReasons: "nope", + audit: null, + } as never, + { sampleSize: -1 }, + ); + expect(payload.enabled).toBe(false); + expect(payload.combinedAccuracy).toBeNull(); + expect(payload.baselineAccuracy).toBe(0); + expect(payload.deltaFromBaseline).toBeNull(); + expect(payload.replayHarnessStatus).toBe("missing"); + expect(payload.holdReasons).toEqual([]); + expect(payload.contributingSources).toEqual([]); + expect(payload.replayRunId).toBeNull(); + expect(payload.observedAt).toBeNull(); + expect(payload.replaySampleSize).toBe(0); + }); + + it("treats a present-but-non-array audit.contributingSources as empty and maps the hold/due true flags", () => { + const payload = snapshotPayloadFromResult( + fakeResult({ audit: { contributingSources: "x" }, replayHarnessHold: true, replayRunDue: true }) as never, + ); + expect(payload.contributingSources).toEqual([]); + expect(payload.replayHarnessHold).toBe(true); + expect(payload.replayRunDue).toBe(true); + }); +}); + +describe("normalizeCalibrationSnapshotPayload (#4248)", () => { + it("accepts a complete valid payload", () => { + expect(normalizeCalibrationSnapshotPayload(validPayload())).toEqual(validPayload()); + }); + + it("rejects a non-object, and every malformed required field", () => { + for (const bad of [ + null, + "x", + [validPayload()], + validPayload({ combinedAccuracy: "x" }), + validPayload({ baselineAccuracy: Number.NaN }), + validPayload({ baselineAccuracy: undefined }), + validPayload({ deltaFromBaseline: "x" }), + validPayload({ autonomyIncreasePermitted: 1 }), + validPayload({ replayHarnessStatus: " " }), + validPayload({ holdReasons: "no" }), + validPayload({ holdReasons: ["ok", 7] }), + ]) { + expect(normalizeCalibrationSnapshotPayload(bad)).toBeNull(); + } + }); + + it("accepts a null combinedAccuracy and a null deltaFromBaseline (warming-up install) and the hold/due true flags", () => { + const normalized = normalizeCalibrationSnapshotPayload( + validPayload({ combinedAccuracy: null, deltaFromBaseline: null, replayHarnessHold: true, replayRunDue: true }), + ); + expect(normalized?.combinedAccuracy).toBeNull(); + expect(normalized?.deltaFromBaseline).toBeNull(); + expect(normalized?.replayHarnessHold).toBe(true); + expect(normalized?.replayRunDue).toBe(true); + }); + + it("filters non-string contributingSources, coerces a non-array to [], and defaults a bad replaySampleSize/enabled", () => { + const normalized = normalizeCalibrationSnapshotPayload( + validPayload({ contributingSources: ["a", 2, "b"], replaySampleSize: -3, enabled: "yes", replayRunId: " " }), + ); + expect(normalized?.contributingSources).toEqual(["a", "b"]); + expect(normalized?.replaySampleSize).toBe(0); + expect(normalized?.enabled).toBe(false); + expect(normalized?.replayRunId).toBeNull(); + + const noArray = normalizeCalibrationSnapshotPayload(validPayload({ contributingSources: 5 })); + expect(noArray?.contributingSources).toEqual([]); + }); +}); + +describe("recordCalibrationSnapshot (#4248)", () => { + it("throws only when the injected ledger is unusable", () => { + expect(() => recordCalibrationSnapshot(validPayload())).toThrow("invalid_event_ledger"); + expect(() => recordCalibrationSnapshot(validPayload(), { eventLedger: {} } as never)).toThrow( + "invalid_event_ledger", + ); + }); + + it("fail-soft returns null for a malformed payload, without appending", () => { + const ledger = mockLedger(); + expect(recordCalibrationSnapshot({ combinedAccuracy: "x" }, { eventLedger: ledger })).toBeNull(); + expect(ledger._events).toHaveLength(0); + }); + + it("appends a repo-scoped event when a repo is given, and an unscoped event otherwise", () => { + const ledger = mockLedger(); + const scoped = recordCalibrationSnapshot(validPayload(), { + eventLedger: ledger, + repoFullName: " acme/widgets ", + }) as unknown as Record; + expect(scoped.type).toBe(MINER_CALIBRATION_SNAPSHOT_EVENT); + expect(scoped.repoFullName).toBe("acme/widgets"); + + const unscoped = recordCalibrationSnapshot(validPayload(), { eventLedger: ledger }) as unknown as Record< + string, + unknown + >; + expect(unscoped.repoFullName).toBeNull(); + expect(ledger._events).toHaveLength(2); + }); +}); + +describe("readCalibrationSnapshots / latestCalibrationSnapshot (#4248)", () => { + it("reduces the append-only stream, skipping foreign types and malformed payloads", () => { + const ledger = mockLedger(); + ledger._events.push( + { type: "pr_outcome", repoFullName: "acme/widgets", payload: validPayload(), seq: 1 }, // foreign type + { type: MINER_CALIBRATION_SNAPSHOT_EVENT, repoFullName: "acme/widgets", payload: { bad: true }, seq: 2 }, // malformed + { + type: MINER_CALIBRATION_SNAPSHOT_EVENT, + repoFullName: 99, // non-string repo → null + payload: validPayload({ combinedAccuracy: 0.7 }), + seq: "x", // non-int seq → null + createdAt: 5, // non-string → null + }, + { + type: MINER_CALIBRATION_SNAPSHOT_EVENT, + repoFullName: "acme/widgets", + payload: validPayload({ combinedAccuracy: 0.8 }), + seq: 4, + createdAt: "2026-07-09T00:00:00.000Z", + }, + ); + const snapshots = readCalibrationSnapshots(ledger); + expect(snapshots.map((s) => s.combinedAccuracy)).toEqual([0.7, 0.8]); + expect(snapshots[0]).toMatchObject({ repoFullName: null, seq: null, createdAt: null }); + expect(snapshots[1]).toMatchObject({ repoFullName: "acme/widgets", seq: 4, createdAt: "2026-07-09T00:00:00.000Z" }); + expect(latestCalibrationSnapshot(ledger)?.combinedAccuracy).toBe(0.8); + }); + + it("reduces to empty for a nullish / unreadable ledger or a non-array read; latest is null", () => { + expect(readCalibrationSnapshots(null as never)).toEqual([]); + expect(readCalibrationSnapshots({} as never)).toEqual([]); + expect(readCalibrationSnapshots({ readEvents: () => null } as never)).toEqual([]); + expect(latestCalibrationSnapshot({ readEvents: () => [] } as never)).toBeNull(); + }); +}); + +describe("runHistoricalReplayCalibrationCycle (#4248)", () => { + const enabledConfig = { miner: { calibration: { phase7LoopEnabled: true, prOutcomeMinDecided: 1 } } }; + const prOutcome = { mergeConfirmed: 8, mergeFalse: 2, closeConfirmed: 0, closeFalse: 0 }; + + it("wires replay scorer → real engine combine → persisted snapshot (the default combine branch)", () => { + const ledger = mockLedger(); + const out = runHistoricalReplayCalibrationCycle( + { + config: enabledConfig, + prOutcome, + replayRun: { + replayResults: [FEAT_REPLAY], + replayRunId: "run-1", + observedAt: "2026-07-09T00:00:00.000Z", + harnessStatus: "healthy", + }, + now: "2026-07-10T00:00:00.000Z", + repoFullName: "acme/widgets", + }, + { eventLedger: ledger }, + ); + // pr_outcome accuracy 0.8, historical_replay accuracy 1.0, default 0.5/0.5 weights → combined 0.9. + expect(out.result.combinedAccuracy).toBe(0.9); + expect(out.result.autonomyIncreasePermitted).toBe(true); + expect(out.compositeScore).toBe(1); + expect(out.sampleSize).toBe(1); + expect(out.snapshot.combinedAccuracy).toBe(0.9); + expect(out.snapshot.replayRunId).toBe("run-1"); + expect(out.recorded).not.toBeNull(); + expect(readCalibrationSnapshots(ledger, { repoFullName: "acme/widgets" })).toHaveLength(1); + }); + + it("uses an injected combine and does not persist when no ledger is injected", () => { + const computeLoop = vi.fn(() => fakeResult({ combinedAccuracy: 0.5 }) as never); + const out = runHistoricalReplayCalibrationCycle( + { replayRun: { replayResults: [FEAT_REPLAY], replayRunId: "r", observedAt: "o", harnessStatus: "healthy" } }, + { computeLoop }, + ); + expect(computeLoop).toHaveBeenCalledOnce(); + expect(out.recorded).toBeNull(); + expect(out.snapshot.combinedAccuracy).toBe(0.5); + // observedAt falls back to the replay run's observedAt when the input carries none. + expect(out.snapshot.observedAt).toBe("o"); + }); + + it("runs with fully-defaulted deps (real engine combine, no ledger) and records nothing", () => { + const out = runHistoricalReplayCalibrationCycle({ config: enabledConfig, prOutcome }); + expect(out.recorded).toBeNull(); + expect(out.historicalReplay).toBeNull(); + expect(typeof out.result.enabled).toBe("boolean"); + }); + + it("prefers an explicit input.observedAt, and falls back to null with no replay run at all", () => { + const computeLoop = vi.fn(() => fakeResult() as never); + const withOverride = runHistoricalReplayCalibrationCycle( + { replayRun: { replayResults: [FEAT_REPLAY], observedAt: "run-time", harnessStatus: "healthy" }, observedAt: "override" }, + { computeLoop }, + ); + expect(withOverride.snapshot.observedAt).toBe("override"); + + const noReplay = runHistoricalReplayCalibrationCycle(undefined, { computeLoop }); + expect(noReplay.historicalReplay).toBeNull(); + expect(noReplay.snapshot.observedAt).toBeNull(); + expect(noReplay.snapshot.replayRunId).toBeNull(); + expect(noReplay.sampleSize).toBe(0); + }); +});