@@ -309,6 +309,34 @@ pub struct CoverageResponse {
309309 pub coverage : Vec < CoverageRow > ,
310310}
311311
312+ /// Query parameters for `GET /api/v1/proof_attempts/trend`.
313+ #[ derive( Debug , Deserialize ) ]
314+ pub struct TrendQuery {
315+ #[ serde( rename = "class" ) ]
316+ pub obligation_class : String ,
317+ pub prover : String ,
318+ /// Bucket size in hours (default 24, max 168 = 1 week).
319+ pub bucket_hours : Option < u32 > ,
320+ /// Max buckets to return (default 90, max 500).
321+ pub limit : Option < u32 > ,
322+ }
323+
324+ #[ derive( Debug , Serialize ) ]
325+ pub struct TrendPoint {
326+ pub bucket_start : String ,
327+ pub success_count : u64 ,
328+ pub total_attempts : u64 ,
329+ pub success_rate : f64 ,
330+ }
331+
332+ #[ derive( Debug , Serialize ) ]
333+ pub struct TrendResponse {
334+ pub obligation_class : String ,
335+ pub prover : String ,
336+ pub bucket_hours : u32 ,
337+ pub points : Vec < TrendPoint > ,
338+ }
339+
312340// ---------------------------------------------------------------------------
313341// V4: Certificate types
314342// ---------------------------------------------------------------------------
@@ -637,6 +665,119 @@ pub async fn list_attempts_handler(
637665 } ) )
638666}
639667
668+ /// `GET /api/v1/proof_attempts/trend?class=X&prover=Y&bucket_hours=24`
669+ ///
670+ /// W11: per-prover success-rate trend over time (hourly or daily buckets).
671+ /// Returns a time series so UIs can chart prover drift.
672+ #[ instrument]
673+ pub async fn trend_handler (
674+ Query ( params) : Query < TrendQuery > ,
675+ ) -> Result < Json < TrendResponse > , HandlerError > {
676+ require_non_empty ( & params. obligation_class , "class" ) ?;
677+ require_non_empty ( & params. prover , "prover" ) ?;
678+ let bucket_hours = params. bucket_hours . unwrap_or ( 24 ) . min ( 168 ) ;
679+ let limit = params. limit . unwrap_or ( 90 ) . min ( 500 ) ;
680+
681+ let sql = format ! (
682+ "SELECT \
683+ toString(toStartOfInterval(started_at, INTERVAL {} HOUR)) AS bucket_start, \
684+ countIf(outcome = 'success') AS success_count, \
685+ count() AS total_attempts \
686+ FROM verisim.proof_attempts \
687+ WHERE obligation_class = '{}' AND prover_used = '{}' \
688+ GROUP BY bucket_start \
689+ ORDER BY bucket_start DESC \
690+ LIMIT {} \
691+ FORMAT JSONEachRow",
692+ bucket_hours,
693+ escape_sql_str( & params. obligation_class) ,
694+ escape_sql_str( & params. prover) ,
695+ limit
696+ ) ;
697+
698+ let body = send_clickhouse_read ( & sql) . await ?;
699+ let points: Vec < TrendPoint > = body
700+ . lines ( )
701+ . filter ( |l| !l. trim ( ) . is_empty ( ) )
702+ . filter_map ( |line| {
703+ let v: serde_json:: Value = serde_json:: from_str ( line) . ok ( ) ?;
704+ let n = u64_from_json ( & v[ "total_attempts" ] ) ?;
705+ let s = u64_from_json ( & v[ "success_count" ] ) ?;
706+ Some ( TrendPoint {
707+ bucket_start : v[ "bucket_start" ]
708+ . as_str ( ) ?
709+ . replacen ( ' ' , "T" , 1 ) ,
710+ success_count : s,
711+ total_attempts : n,
712+ success_rate : if n > 0 { ( s as f64 ) / ( n as f64 ) } else { 0.0 } ,
713+ } )
714+ } )
715+ . collect ( ) ;
716+
717+ Ok ( Json ( TrendResponse {
718+ obligation_class : params. obligation_class ,
719+ prover : params. prover ,
720+ bucket_hours,
721+ points,
722+ } ) )
723+ }
724+
725+ /// `GET /api/v1/proof_attempts/metrics` — Prometheus-format cert events.
726+ ///
727+ /// W10: exposes cert counters in Prometheus text format. Call from a
728+ /// Prometheus scrape target every N seconds to time-series the
729+ /// mint/revoke cycle.
730+ #[ instrument]
731+ pub async fn cert_metrics_handler ( ) -> Result < String , HandlerError > {
732+ let mut output = String :: new ( ) ;
733+
734+ // Count proven certs
735+ let proven_count_sql = "SELECT count() FROM verisim.mv_proven_certificates \
736+ WHERE status = 'proven' FORMAT TSV";
737+ let proven_body = send_clickhouse_read ( proven_count_sql)
738+ . await
739+ . unwrap_or_default ( ) ;
740+ let proven_n: u64 = proven_body. trim ( ) . parse ( ) . unwrap_or ( 0 ) ;
741+
742+ // Count pending certs
743+ let pending_count_sql = "SELECT count() FROM verisim.mv_proven_certificates \
744+ WHERE status = 'pending' FORMAT TSV";
745+ let pending_body = send_clickhouse_read ( pending_count_sql)
746+ . await
747+ . unwrap_or_default ( ) ;
748+ let pending_n: u64 = pending_body. trim ( ) . parse ( ) . unwrap_or ( 0 ) ;
749+
750+ // Count sanctified classes
751+ let sanctified_count_sql = "SELECT count() FROM verisim.mv_sanctify_certificates \
752+ WHERE status = 'sanctified' FORMAT TSV";
753+ let sanctified_body = send_clickhouse_read ( sanctified_count_sql)
754+ . await
755+ . unwrap_or_default ( ) ;
756+ let sanctified_n: u64 = sanctified_body. trim ( ) . parse ( ) . unwrap_or ( 0 ) ;
757+
758+ // Count revocations
759+ let revoked_count_sql = "SELECT count() FROM verisim.mv_cert_revocations FORMAT TSV" ;
760+ let revoked_body = send_clickhouse_read ( revoked_count_sql)
761+ . await
762+ . unwrap_or_default ( ) ;
763+ let revoked_n: u64 = revoked_body. trim ( ) . parse ( ) . unwrap_or ( 0 ) ;
764+
765+ output. push_str ( "# HELP verisim_cert_proven_total Total PROVEN certificates\n " ) ;
766+ output. push_str ( "# TYPE verisim_cert_proven_total gauge\n " ) ;
767+ output. push_str ( & format ! ( "verisim_cert_proven_total {}\n " , proven_n) ) ;
768+ output. push_str ( "# HELP verisim_cert_pending_total Total pending certificates\n " ) ;
769+ output. push_str ( "# TYPE verisim_cert_pending_total gauge\n " ) ;
770+ output. push_str ( & format ! ( "verisim_cert_pending_total {}\n " , pending_n) ) ;
771+ output. push_str ( "# HELP verisim_cert_sanctified_total Total SANCTIFIED classes\n " ) ;
772+ output. push_str ( "# TYPE verisim_cert_sanctified_total gauge\n " ) ;
773+ output. push_str ( & format ! ( "verisim_cert_sanctified_total {}\n " , sanctified_n) ) ;
774+ output. push_str ( "# HELP verisim_cert_revoked_total Certs proven all-time but pending in 30-day window\n " ) ;
775+ output. push_str ( "# TYPE verisim_cert_revoked_total gauge\n " ) ;
776+ output. push_str ( & format ! ( "verisim_cert_revoked_total {}\n " , revoked_n) ) ;
777+
778+ Ok ( output)
779+ }
780+
640781/// `GET /api/v1/proof_attempts/coverage`
641782///
642783/// Returns cross-repo coverage stats per obligation_class: how many distinct
0 commit comments