Skip to content

Commit a253bb1

Browse files
hyperpolymathclaude
andcommitted
feat: implement corpus_monitor for corpus health tracking on proof additions
- Create CorpusMonitor to track corpus metrics (proofs, premises, size) - Monitor corpus growth and size change percentages - Update health.corpus_health on proof additions - Add 8 unit tests for corpus monitor functionality - Add 8 integration tests validating health status updates - Track metrics: total_proofs, total_premises, size_mb, size_change_percent, last_updated Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent fcb05ce commit a253bb1

3 files changed

Lines changed: 485 additions & 0 deletions

File tree

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Monitor corpus health: proofs, premises, size metrics
3+
4+
use chrono::{DateTime, Utc};
5+
use serde::{Deserialize, Serialize};
6+
use std::fs;
7+
use std::path::Path;
8+
use std::sync::{Arc, Mutex};
9+
10+
/// Snapshot of corpus metrics at a point in time
11+
#[derive(Debug, Clone, Serialize, Deserialize)]
12+
pub struct CorpusMetrics {
13+
/// Total number of proof records
14+
pub total_proofs: usize,
15+
/// Total number of premises/tactics
16+
pub total_premises: usize,
17+
/// Total corpus size in MB
18+
pub size_mb: f64,
19+
/// Percentage change since last check
20+
pub size_change_percent: f64,
21+
/// Timestamp of last update
22+
pub last_updated: DateTime<Utc>,
23+
}
24+
25+
impl Default for CorpusMetrics {
26+
fn default() -> Self {
27+
Self {
28+
total_proofs: 0,
29+
total_premises: 0,
30+
size_mb: 0.0,
31+
size_change_percent: 0.0,
32+
last_updated: Utc::now(),
33+
}
34+
}
35+
}
36+
37+
/// Monitor for corpus health and growth
38+
///
39+
/// Tracks:
40+
/// - Number of proofs and premises in the corpus
41+
/// - Size metrics (MB)
42+
/// - Growth rates and trends
43+
pub struct CorpusMonitor {
44+
/// Path to training data directory
45+
training_data_dir: String,
46+
/// Current metrics
47+
current: Arc<Mutex<CorpusMetrics>>,
48+
/// Previous metrics (for computing change %)
49+
previous: Arc<Mutex<CorpusMetrics>>,
50+
}
51+
52+
impl CorpusMonitor {
53+
/// Create a new corpus monitor for the given training data directory
54+
pub fn new(training_data_dir: impl Into<String>) -> Self {
55+
Self {
56+
training_data_dir: training_data_dir.into(),
57+
current: Arc::new(Mutex::new(CorpusMetrics::default())),
58+
previous: Arc::new(Mutex::new(CorpusMetrics::default())),
59+
}
60+
}
61+
62+
/// Scan the corpus directory and update metrics
63+
pub fn scan_corpus(&self) -> Result<CorpusMetrics, String> {
64+
let path = Path::new(&self.training_data_dir);
65+
66+
if !path.exists() {
67+
return Err(format!(
68+
"Training data directory not found: {}",
69+
self.training_data_dir
70+
));
71+
}
72+
73+
let mut total_proofs = 0;
74+
let mut total_premises = 0;
75+
let mut total_size_bytes = 0u64;
76+
77+
// Scan for premises_*.jsonl files
78+
match fs::read_dir(path) {
79+
Ok(entries) => {
80+
for entry in entries {
81+
if let Ok(entry) = entry {
82+
let path = entry.path();
83+
let filename = path
84+
.file_name()
85+
.and_then(|n| n.to_str())
86+
.unwrap_or("");
87+
88+
if filename.starts_with("premises_") && filename.ends_with(".jsonl") {
89+
if let Ok(metadata) = fs::metadata(&path) {
90+
total_size_bytes += metadata.len();
91+
92+
// Count lines (proofs) in the file
93+
if let Ok(content) = fs::read_to_string(&path) {
94+
let line_count = content.lines().count();
95+
total_proofs += line_count;
96+
// Estimate premises as 2-3 per proof on average
97+
total_premises += line_count * 2;
98+
}
99+
}
100+
}
101+
}
102+
}
103+
}
104+
Err(e) => {
105+
return Err(format!("Failed to read corpus directory: {}", e));
106+
}
107+
}
108+
109+
// Calculate size change percentage
110+
let size_mb = total_size_bytes as f64 / (1024.0 * 1024.0);
111+
let size_change_percent = if let Ok(prev) = self.previous.lock() {
112+
if prev.size_mb > 0.0 {
113+
((size_mb - prev.size_mb) / prev.size_mb) * 100.0
114+
} else {
115+
0.0
116+
}
117+
} else {
118+
0.0
119+
};
120+
121+
let metrics = CorpusMetrics {
122+
total_proofs,
123+
total_premises,
124+
size_mb,
125+
size_change_percent,
126+
last_updated: Utc::now(),
127+
};
128+
129+
// Update internal state
130+
if let Ok(mut curr) = self.current.lock() {
131+
if let Ok(mut prev) = self.previous.lock() {
132+
*prev = (*curr).clone();
133+
}
134+
*curr = metrics.clone();
135+
}
136+
137+
Ok(metrics)
138+
}
139+
140+
/// Get current corpus metrics
141+
pub fn metrics(&self) -> CorpusMetrics {
142+
self.current
143+
.lock()
144+
.ok()
145+
.map(|m| m.clone())
146+
.unwrap_or_default()
147+
}
148+
149+
/// Add a new proof record to the corpus count
150+
pub fn add_proof(&self, premise_count: usize) {
151+
if let Ok(mut metrics) = self.current.lock() {
152+
metrics.total_proofs += 1;
153+
metrics.total_premises += premise_count;
154+
metrics.last_updated = Utc::now();
155+
}
156+
}
157+
158+
/// Update corpus size estimate
159+
pub fn update_size(&self, size_mb: f64) {
160+
if let Ok(mut metrics) = self.current.lock() {
161+
let prev_size = metrics.size_mb;
162+
metrics.size_mb = size_mb;
163+
if prev_size > 0.0 {
164+
metrics.size_change_percent = ((size_mb - prev_size) / prev_size) * 100.0;
165+
}
166+
metrics.last_updated = Utc::now();
167+
}
168+
}
169+
170+
/// Get total number of proofs
171+
pub fn total_proofs(&self) -> usize {
172+
self.current
173+
.lock()
174+
.ok()
175+
.map(|m| m.total_proofs)
176+
.unwrap_or(0)
177+
}
178+
179+
/// Get total number of premises
180+
pub fn total_premises(&self) -> usize {
181+
self.current
182+
.lock()
183+
.ok()
184+
.map(|m| m.total_premises)
185+
.unwrap_or(0)
186+
}
187+
188+
/// Get corpus size in MB
189+
pub fn size_mb(&self) -> f64 {
190+
self.current
191+
.lock()
192+
.ok()
193+
.map(|m| m.size_mb)
194+
.unwrap_or(0.0)
195+
}
196+
197+
/// Get size change percentage since last check
198+
pub fn size_change_percent(&self) -> f64 {
199+
self.current
200+
.lock()
201+
.ok()
202+
.map(|m| m.size_change_percent)
203+
.unwrap_or(0.0)
204+
}
205+
206+
/// Reset metrics to default
207+
pub fn reset(&self) {
208+
if let Ok(mut metrics) = self.current.lock() {
209+
*metrics = CorpusMetrics::default();
210+
}
211+
}
212+
}
213+
214+
#[cfg(test)]
215+
mod tests {
216+
use super::*;
217+
use tempfile::TempDir;
218+
219+
#[test]
220+
fn test_corpus_monitor_creation() {
221+
let monitor = CorpusMonitor::new("./test_data");
222+
assert_eq!(monitor.total_proofs(), 0);
223+
assert_eq!(monitor.total_premises(), 0);
224+
}
225+
226+
#[test]
227+
fn test_add_proof() {
228+
let monitor = CorpusMonitor::new("./test_data");
229+
monitor.add_proof(3);
230+
monitor.add_proof(5);
231+
232+
assert_eq!(monitor.total_proofs(), 2);
233+
assert_eq!(monitor.total_premises(), 8); // 3 + 5
234+
}
235+
236+
#[test]
237+
fn test_update_size() {
238+
let monitor = CorpusMonitor::new("./test_data");
239+
monitor.update_size(100.0);
240+
241+
let metrics = monitor.metrics();
242+
assert_eq!(metrics.size_mb, 100.0);
243+
assert_eq!(metrics.size_change_percent, 0.0); // First update
244+
245+
monitor.update_size(150.0);
246+
let metrics = monitor.metrics();
247+
assert!(metrics.size_change_percent > 0.0); // Should show growth
248+
assert!((metrics.size_change_percent - 50.0).abs() < 0.1); // 50% growth
249+
}
250+
251+
#[test]
252+
fn test_scan_corpus_no_directory() {
253+
let monitor = CorpusMonitor::new("/nonexistent/path");
254+
assert!(monitor.scan_corpus().is_err());
255+
}
256+
257+
#[test]
258+
fn test_scan_corpus_empty_directory() {
259+
if let Ok(temp_dir) = TempDir::new() {
260+
let path = temp_dir.path().to_str().unwrap();
261+
let monitor = CorpusMonitor::new(path);
262+
263+
// Should succeed but return zero counts
264+
match monitor.scan_corpus() {
265+
Ok(metrics) => {
266+
assert_eq!(metrics.total_proofs, 0);
267+
assert_eq!(metrics.total_premises, 0);
268+
assert_eq!(metrics.size_mb, 0.0);
269+
}
270+
Err(e) => panic!("Failed to scan empty directory: {}", e),
271+
}
272+
}
273+
}
274+
275+
#[test]
276+
fn test_corpus_metrics_timestamp() {
277+
let monitor = CorpusMonitor::new("./test_data");
278+
monitor.add_proof(1);
279+
280+
let metrics = monitor.metrics();
281+
let now = Utc::now();
282+
let diff = now.signed_duration_since(metrics.last_updated);
283+
// Timestamp should be recent (within a few seconds)
284+
assert!(diff.num_seconds() < 5);
285+
}
286+
287+
#[test]
288+
fn test_corpus_metrics_serialization() {
289+
let metrics = CorpusMetrics {
290+
total_proofs: 1000,
291+
total_premises: 3000,
292+
size_mb: 512.5,
293+
size_change_percent: 5.5,
294+
last_updated: Utc::now(),
295+
};
296+
297+
// Verify metrics can be serialized to JSON
298+
let json = serde_json::to_string(&metrics).expect("Should serialize");
299+
assert!(json.contains("1000"));
300+
assert!(json.contains("3000"));
301+
}
302+
303+
#[test]
304+
fn test_reset_corpus() {
305+
let monitor = CorpusMonitor::new("./test_data");
306+
monitor.add_proof(5);
307+
monitor.update_size(250.0);
308+
309+
assert!(monitor.total_proofs() > 0);
310+
assert!(monitor.size_mb() > 0.0);
311+
312+
monitor.reset();
313+
assert_eq!(monitor.total_proofs(), 0);
314+
assert_eq!(monitor.total_premises(), 0);
315+
}
316+
}

src/rust/diagnostics/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// SPDX-License-Identifier: PMPL-1.0-or-later
22
// Health diagnostics and monitoring for echidna
33

4+
pub mod corpus_monitor;
45
pub mod health;
56
pub mod gnn_training;
67

8+
pub use corpus_monitor::{CorpusMetrics, CorpusMonitor};
79
pub use health::{HealthStatus, ProverHealth, ModelHealth, CorpusHealth, DegradationMode};
810
pub use gnn_training::{GnnTrainingMetrics, load_training_metrics, update_health_with_metrics};

0 commit comments

Comments
 (0)