|
| 1 | +// Predicted-gate call history (#predicted-live-gate-agreement, maintainer review-stack x AMS integration |
| 2 | +// audit 2026-07-09) -- records EVERY MCP `gittensory_predict_gate`/`gittensory_explain_gate_disposition` call, |
| 3 | +// so a later real gate decision for the same (project, login) can be paired against it (see |
| 4 | +// src/review/predicted-gate-agreement.ts for the read/join side). Structurally a sibling of |
| 5 | +// src/review/contributor-calibration.ts: `review_audit` (migrations/0049) is DELIBERATELY actor-login-free |
| 6 | +// (feeds the anonymized orb-collector export), so this is its own separate, LOCAL-ONLY table |
| 7 | +// (migrations/0132) -- never wired into exportOrbBatch or any other cross-instance/public export path. |
| 8 | +// |
| 9 | +// UNLIKE contributor-calibration.ts's per-commit dedup (a re-run at the same head_sha replaces its prior row), |
| 10 | +// every predict_gate call gets its OWN row here: there is no commit to dedup against pre-submission, and a |
| 11 | +// miner iterating on the same repo (tweaking a title, retrying after a blocker) makes a genuinely new inquiry |
| 12 | +// each time -- collapsing them would undercount how often the tool was actually consulted. |
| 13 | + |
| 14 | +import { isParityAuditEnabled, nativeGateActionFromConclusion } from "./parity-wire"; |
| 15 | +import type { GateCheckConclusion } from "../rules/advisory"; |
| 16 | +import { isSelfHostedReviewRuntime } from "../selfhost/review-runtime"; |
| 17 | +import { errorMessage, nowIso } from "../utils/json"; |
| 18 | + |
| 19 | +/** The minimal env shape the recorder needs -- mirrors parity-wire.ts's ParityRecorderEnv / contributor- |
| 20 | + * calibration.ts's ContributorCalibrationEnv exactly, since this records under the identical self-hosted/ |
| 21 | + * parity-flag gate (one flag controls the whole gate-accuracy telemetry family). */ |
| 22 | +type PredictedGateCallEnv = { |
| 23 | + DB: D1Database; |
| 24 | + GITTENSORY_REVIEW_PARITY_AUDIT?: string | undefined; |
| 25 | + SELFHOST_TRANSIENT_CACHE?: NonNullable<Env["SELFHOST_TRANSIENT_CACHE"]>; |
| 26 | +}; |
| 27 | + |
| 28 | +/** The minimal verdict shape this recorder needs -- structurally compatible with PredictedGateVerdict |
| 29 | + * (packages/gittensory-engine), whose `blockers` entries are the public-safe shape (no `severity`), unlike |
| 30 | + * the real gate's AdvisoryFinding -- so this reads only `.code`, never reusing neutralHoldReasonCode's |
| 31 | + * stricter AdvisoryFinding-typed signature (see the reasonCode comment below for why that's an acceptable, |
| 32 | + * deliberately coarser fallback on the predicted side). */ |
| 33 | +type RecordablePredictedVerdict = { |
| 34 | + conclusion: GateCheckConclusion; |
| 35 | + blockers: Array<{ code: string }>; |
| 36 | +}; |
| 37 | + |
| 38 | +/** |
| 39 | + * Record one MCP predict_gate/explain_gate_disposition call into `predicted_gate_calls`, keyed by the |
| 40 | + * requested contributor's login. Gated identically to {@link recordNativeGateDecision} in parity-wire.ts (same |
| 41 | + * self-hosted-always-records / cloud-flag-gated contract) -- this is additive telemetry alongside the same |
| 42 | + * gate-accuracy measurement family, not a separate feature with its own on/off knob. |
| 43 | + * |
| 44 | + * Best-effort: a write failure is swallowed (telemetry must never break the MCP tool response). A missing/ |
| 45 | + * empty login records nothing -- there is no meaningful per-actor row to write without one. |
| 46 | + */ |
| 47 | +export async function recordPredictedGateCall( |
| 48 | + env: PredictedGateCallEnv, |
| 49 | + input: { login: string | null | undefined; project: string; verdict: RecordablePredictedVerdict }, |
| 50 | +): Promise<void> { |
| 51 | + if (!isSelfHostedReviewRuntime(env) && !isParityAuditEnabled(env)) return; |
| 52 | + const login = input.login?.trim(); |
| 53 | + if (!login) return; |
| 54 | + const action = nativeGateActionFromConclusion(input.verdict.conclusion); |
| 55 | + if (action === null) return; // "skipped" -- not a comparable prediction (mirrors recordNativeGateDecision) |
| 56 | + const project = input.project.slice(0, 200); |
| 57 | + // Coarser than the real gate_decision's summary (which recovers a specific neutral-hold sub-code via |
| 58 | + // neutralHoldReasonCode): the predicted-gate engine's public verdict shape carries no `severity` on its |
| 59 | + // findings, so it isn't AdvisoryFinding-shaped and can't reuse that stricter-typed helper. reason_code here |
| 60 | + // is an observability aid only (not read by computePredictedGateAgreement's core comparison), so the bare |
| 61 | + // conclusion string is an acceptable fallback for every non-failure case. |
| 62 | + const reasonCode = input.verdict.conclusion === "failure" ? (input.verdict.blockers[0]?.code ?? input.verdict.conclusion) : input.verdict.conclusion; |
| 63 | + try { |
| 64 | + // Every call gets its own row (no dedup key) -- see the module header for why, unlike |
| 65 | + // recordContributorGateDecision's per-commit replace. |
| 66 | + await env.DB.prepare( |
| 67 | + `INSERT INTO predicted_gate_calls (id, login, project, predicted_action, conclusion, reason_code, created_at) |
| 68 | + VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 69 | + ) |
| 70 | + .bind(`predicted:${login}:${project}:${nowIso()}:${Math.random().toString(36).slice(2, 8)}`, login, project, action, input.verdict.conclusion, reasonCode.slice(0, 200), nowIso()) |
| 71 | + .run(); |
| 72 | + } catch (error) { |
| 73 | + console.warn(JSON.stringify({ event: "predicted_gate_calls_record_error", project, message: errorMessage(error).slice(0, 200) })); |
| 74 | + } |
| 75 | +} |
0 commit comments