Skip to content

Commit 77446fe

Browse files
hyperpolymathclaude
andcommitted
feat(api): W6+W7 — row-level GET /proof_attempts with filters
Adds GET /api/v1/proof_attempts returning individual attempt rows (vs aggregates from /strategy or /certificates/evidence cursors). Query params (all optional): class=X — filter by obligation_class prover=Y — filter by prover_used outcome=Z — filter by outcome (success|failure|timeout|unknown) repo=W — filter by repo limit=N — cap at 1000, default 100 offset=M — pagination Returns AttemptsResponse { attempts: [AttemptRow], total_returned, offset, limit }. Each row carries attempt_id, obligation_id, repo, file, obligation_class, prover_used, outcome, duration_ms, confidence, strategy_tag, started_at (ISO-8601). Unblocks two downstream consumers: * Hypatia.Neural.ProverRecommender can now train on real feature vectors extracted from individual attempts, rather than unfolding synthetic rows from /strategy aggregates. * Hypatia.Rules.StrategyDrift can fetch the complete failed-attempt set for (class, old_prover) via outcome=failure, replacing the ≤100-row evidence-cursor approximation that used /certificates. Subsumes the separate /failed_attempts endpoint entirely. Verified live: ?class=equiv&prover=coq&outcome=failure → 3 rows, first attempt_id 077962cf, ephapax/Semantics.v ?class=safety&limit=2 → 2 rows, provers=[cvc5,eprover] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a4b5453 commit 77446fe

2 files changed

Lines changed: 130 additions & 1 deletion

File tree

rust-core/verisim-api/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,8 @@ pub fn build_router(state: AppState) -> Router {
834834
// Proof-attempts endpoints — ClickHouse-backed, no AppState required
835835
.route(
836836
"/api/v1/proof_attempts",
837-
post(proof_attempts::insert_handler),
837+
post(proof_attempts::insert_handler)
838+
.get(proof_attempts::list_attempts_handler),
838839
)
839840
.route(
840841
"/api/v1/proof_attempts/strategy",

rust-core/verisim-api/src/proof_attempts.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,49 @@ pub struct InsertResponse {
221221
pub attempt_id: String,
222222
}
223223

224+
/// Query parameters for `GET /api/v1/proof_attempts` (row-level).
225+
#[derive(Debug, Deserialize)]
226+
pub struct AttemptsQuery {
227+
/// Optional filter: obligation class
228+
#[serde(rename = "class")]
229+
pub obligation_class: Option<String>,
230+
/// Optional filter: prover
231+
pub prover: Option<String>,
232+
/// Optional filter: outcome (success/failure/timeout/unknown)
233+
pub outcome: Option<String>,
234+
/// Optional filter: repo
235+
pub repo: Option<String>,
236+
/// Max rows to return (default 100, cap 1000)
237+
pub limit: Option<u32>,
238+
/// Skip this many rows (for pagination)
239+
pub offset: Option<u32>,
240+
}
241+
242+
/// Row-level view of a single proof attempt, suitable for training
243+
/// feature vectors or client-side inspection.
244+
#[derive(Debug, Serialize)]
245+
pub struct AttemptRow {
246+
pub attempt_id: String,
247+
pub obligation_id: String,
248+
pub repo: String,
249+
pub file: String,
250+
pub obligation_class: String,
251+
pub prover_used: String,
252+
pub outcome: String,
253+
pub duration_ms: u64,
254+
pub confidence: f64,
255+
pub strategy_tag: String,
256+
pub started_at: String,
257+
}
258+
259+
#[derive(Debug, Serialize)]
260+
pub struct AttemptsResponse {
261+
pub attempts: Vec<AttemptRow>,
262+
pub total_returned: usize,
263+
pub offset: u32,
264+
pub limit: u32,
265+
}
266+
224267
/// Query parameters for `GET /api/v1/proof_attempts/strategy`.
225268
#[derive(Debug, Deserialize)]
226269
pub struct StrategyQuery {
@@ -509,6 +552,91 @@ pub async fn insert_handler(
509552
))
510553
}
511554

555+
/// `GET /api/v1/proof_attempts` (row-level query)
556+
///
557+
/// Returns individual attempt rows with optional filters. Useful for
558+
/// RBF training (real feature vectors instead of unfolded aggregates)
559+
/// and for the StrategyDrift candidate query (`outcome=failure`
560+
/// + `class=X` + `prover=Y` returns exactly the re-queue candidate
561+
/// set, no 100-row cap from the certificates evidence path).
562+
#[instrument]
563+
pub async fn list_attempts_handler(
564+
Query(params): Query<AttemptsQuery>,
565+
) -> Result<Json<AttemptsResponse>, HandlerError> {
566+
let limit = params.limit.unwrap_or(100).min(1000);
567+
let offset = params.offset.unwrap_or(0);
568+
569+
let mut wheres: Vec<String> = Vec::new();
570+
if let Some(c) = &params.obligation_class {
571+
require_non_empty(c, "class")?;
572+
wheres.push(format!("obligation_class = '{}'", escape_sql_str(c)));
573+
}
574+
if let Some(p) = &params.prover {
575+
require_non_empty(p, "prover")?;
576+
wheres.push(format!("prover_used = '{}'", escape_sql_str(p)));
577+
}
578+
if let Some(o) = &params.outcome {
579+
require_non_empty(o, "outcome")?;
580+
wheres.push(format!("outcome = '{}'", escape_sql_str(o)));
581+
}
582+
if let Some(r) = &params.repo {
583+
require_non_empty(r, "repo")?;
584+
wheres.push(format!("repo = '{}'", escape_sql_str(r)));
585+
}
586+
let where_clause = if wheres.is_empty() {
587+
String::new()
588+
} else {
589+
format!("WHERE {}", wheres.join(" AND "))
590+
};
591+
592+
let sql = format!(
593+
"SELECT attempt_id, obligation_id, repo, file, obligation_class, \
594+
toString(prover_used) AS prover_used, \
595+
toString(outcome) AS outcome, duration_ms, \
596+
toFloat64(confidence) AS confidence, strategy_tag, \
597+
toString(started_at) AS started_at \
598+
FROM verisim.proof_attempts \
599+
{} \
600+
ORDER BY started_at DESC \
601+
LIMIT {} OFFSET {} \
602+
FORMAT JSONEachRow",
603+
where_clause, limit, offset
604+
);
605+
606+
let body = send_clickhouse_read(&sql).await?;
607+
let attempts: Vec<AttemptRow> = body
608+
.lines()
609+
.filter(|l| !l.trim().is_empty())
610+
.filter_map(|line| {
611+
let v: serde_json::Value = serde_json::from_str(line).ok()?;
612+
Some(AttemptRow {
613+
attempt_id: v["attempt_id"].as_str()?.to_string(),
614+
obligation_id: v["obligation_id"].as_str()?.to_string(),
615+
repo: v["repo"].as_str()?.to_string(),
616+
file: v["file"].as_str()?.to_string(),
617+
obligation_class: v["obligation_class"].as_str()?.to_string(),
618+
prover_used: v["prover_used"].as_str()?.to_string(),
619+
outcome: v["outcome"].as_str()?.to_string(),
620+
duration_ms: u64_from_json(&v["duration_ms"])?,
621+
confidence: v["confidence"].as_f64().unwrap_or(0.5),
622+
strategy_tag: v["strategy_tag"].as_str()?.to_string(),
623+
started_at: v["started_at"]
624+
.as_str()
625+
.unwrap_or_default()
626+
.replacen(' ', "T", 1),
627+
})
628+
})
629+
.collect();
630+
631+
let total_returned = attempts.len();
632+
Ok(Json(AttemptsResponse {
633+
attempts,
634+
total_returned,
635+
offset,
636+
limit,
637+
}))
638+
}
639+
512640
/// `GET /api/v1/proof_attempts/coverage`
513641
///
514642
/// Returns cross-repo coverage stats per obligation_class: how many distinct

0 commit comments

Comments
 (0)