1use crate::lab::config::LabConfig;
22use crate::lab::runtime::{InvariantViolation, LabRuntime};
23use crate::trace::boundary::SquareComplex;
24use crate::trace::canonicalize::{trace_fingerprint, TraceMonoid};
25use crate::trace::dpor::detect_races;
26use crate::trace::event::TraceEvent;
27use crate::trace::event_structure::TracePoset;
28use crate::trace::scoring::{
29 score_persistence, seed_fingerprint, ClassId, EvidenceLedger, TopologicalScore,
30};
31use serde::Serialize;
32use std::collections::hash_map::DefaultHasher;
33use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet, VecDeque};
34use std::hash::{Hash, Hasher};
35use std::io;
36use std::path::Path;
37
38const DEFAULT_SATURATION_WINDOW: usize = 10;
39const DEFAULT_UNEXPLORED_LIMIT: usize = 5;
40const DEFAULT_DERIVED_SEEDS: usize = 4;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum ExplorationMode {
45 #[default]
47 Baseline,
48 TopologyPrioritized,
50}
51
52#[derive(Debug, Clone)]
54pub struct ExplorerConfig {
55 pub base_seed: u64,
57 pub max_runs: usize,
59 pub max_steps_per_run: u64,
61 pub worker_count: usize,
63 pub record_traces: bool,
65}
66
67impl Default for ExplorerConfig {
68 fn default() -> Self {
69 Self {
70 base_seed: 0,
71 max_runs: 100,
72 max_steps_per_run: 100_000,
73 worker_count: 1,
74 record_traces: true,
75 }
76 }
77}
78
79impl ExplorerConfig {
80 #[must_use]
82 pub fn new(base_seed: u64, max_runs: usize) -> Self {
83 Self {
84 base_seed,
85 max_runs,
86 ..Default::default()
87 }
88 }
89
90 #[must_use]
92 pub fn worker_count(mut self, n: usize) -> Self {
93 self.worker_count = n;
94 self
95 }
96
97 #[must_use]
99 pub fn max_steps(mut self, n: u64) -> Self {
100 self.max_steps_per_run = n;
101 self
102 }
103}
104
105#[derive(Debug)]
107pub struct RunResult {
108 pub seed: u64,
110 pub steps: u64,
112 pub fingerprint: u64,
114 pub is_new_class: bool,
116 pub violations: Vec<InvariantViolation>,
118 pub certificate_hash: u64,
120}
121
122#[derive(Debug)]
124pub struct ViolationReport {
125 pub seed: u64,
127 pub steps: u64,
129 pub violations: Vec<InvariantViolation>,
131 pub fingerprint: u64,
133}
134
135#[derive(Debug, Clone, Serialize)]
137pub struct CoverageMetrics {
138 pub equivalence_classes: usize,
140 pub total_runs: usize,
142 pub new_class_discoveries: usize,
144 pub class_run_counts: HashMap<u64, usize>,
146 pub novelty_histogram: BTreeMap<u32, usize>,
148 pub saturation: SaturationMetrics,
150}
151
152impl CoverageMetrics {
153 #[must_use]
155 #[allow(clippy::cast_precision_loss)]
156 pub fn discovery_rate(&self) -> f64 {
157 if self.total_runs == 0 {
158 return 0.0;
159 }
160 self.new_class_discoveries as f64 / self.total_runs as f64
161 }
162
163 #[must_use]
165 pub fn is_saturated(&self, window: usize) -> bool {
166 if self.total_runs < window {
167 return false;
168 }
169 self.total_runs - self.new_class_discoveries >= window
170 }
171}
172
173#[derive(Debug, Clone, Serialize)]
175pub struct SaturationMetrics {
176 pub window: usize,
178 pub saturated: bool,
180 pub existing_class_hits: usize,
182 pub runs_since_last_new_class: Option<usize>,
184}
185
186#[derive(Debug, Clone)]
188pub struct UnexploredSeed {
189 pub seed: u64,
191 pub score: Option<TopologicalScore>,
193}
194
195fn novelty_histogram_from_flags(results: &[RunResult]) -> BTreeMap<u32, usize> {
196 let mut histogram = BTreeMap::new();
197 for r in results {
198 let novelty = u32::from(r.is_new_class);
199 *histogram.entry(novelty).or_insert(0) += 1;
200 }
201 histogram
202}
203
204fn novelty_histogram_from_ledgers(ledgers: &[EvidenceLedger]) -> BTreeMap<u32, usize> {
205 let mut histogram = BTreeMap::new();
206 for ledger in ledgers {
207 *histogram.entry(ledger.score.novelty).or_insert(0) += 1;
208 }
209 histogram
210}
211
212fn saturation_metrics(
213 results: &[RunResult],
214 total_runs: usize,
215 new_class_discoveries: usize,
216) -> SaturationMetrics {
217 let existing_class_hits = total_runs.saturating_sub(new_class_discoveries);
218 let runs_since_last_new_class = if results.is_empty() {
219 None
220 } else {
221 let last_new = results.iter().rposition(|r| r.is_new_class);
222 Some(last_new.map_or(results.len(), |idx| results.len() - 1 - idx))
223 };
224 let window = DEFAULT_SATURATION_WINDOW;
225 let saturated = if total_runs < window {
226 false
227 } else {
228 existing_class_hits >= window
229 };
230 SaturationMetrics {
231 window,
232 saturated,
233 existing_class_hits,
234 runs_since_last_new_class,
235 }
236}
237
238#[derive(Debug)]
240pub struct ExplorationReport {
241 pub total_runs: usize,
243 pub unique_classes: usize,
245 pub violations: Vec<ViolationReport>,
247 pub coverage: CoverageMetrics,
249 pub top_unexplored: Vec<UnexploredSeed>,
251 pub runs: Vec<RunResult>,
253}
254
255impl ExplorationReport {
256 #[must_use]
258 pub fn has_violations(&self) -> bool {
259 !self.violations.is_empty()
260 }
261
262 #[must_use]
264 pub fn violation_seeds(&self) -> Vec<u64> {
265 self.violations.iter().map(|v| v.seed).collect()
266 }
267
268 #[must_use]
273 pub fn certificate_divergences(&self) -> Vec<(u64, u64)> {
274 let mut by_class: HashMap<u64, Vec<&RunResult>> = HashMap::new();
275 for r in &self.runs {
276 by_class.entry(r.fingerprint).or_default().push(r);
277 }
278 let mut divergences = Vec::new();
279 for runs in by_class.values() {
280 if runs.len() < 2 {
281 continue;
282 }
283 let reference = runs[0].certificate_hash;
284 for r in &runs[1..] {
285 if r.certificate_hash != reference {
286 divergences.push((runs[0].seed, r.seed));
287 }
288 }
289 }
290 divergences
291 }
292
293 #[must_use]
295 pub fn certificates_consistent(&self) -> bool {
296 self.certificate_divergences().is_empty()
297 }
298
299 #[must_use]
301 pub fn to_json_summary(&self) -> ExplorationReportJson {
302 ExplorationReportJson {
303 total_runs: self.total_runs,
304 unique_classes: self.unique_classes,
305 violations: self
306 .violations
307 .iter()
308 .map(ViolationReport::summary)
309 .collect(),
310 violation_seeds: self.violation_seeds(),
311 coverage: self.coverage.clone(),
312 top_unexplored: self
313 .top_unexplored
314 .iter()
315 .map(UnexploredSeedJson::from_seed)
316 .collect(),
317 runs: self.runs.iter().map(RunResult::summary).collect(),
318 certificate_divergences: self.certificate_divergences(),
319 }
320 }
321
322 pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
324 serde_json::to_string(&self.to_json_summary())
325 }
326
327 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
329 serde_json::to_string_pretty(&self.to_json_summary())
330 }
331
332 pub fn write_json_summary<P: AsRef<Path>>(&self, path: P, pretty: bool) -> io::Result<()> {
336 let json = if pretty {
337 self.to_json_pretty().map_err(json_err)?
338 } else {
339 self.to_json_string().map_err(json_err)?
340 };
341 std::fs::write(path, json)
342 }
343}
344
345fn json_err(err: serde_json::Error) -> io::Error {
346 io::Error::other(err)
347}
348
349#[derive(Debug, Serialize)]
351pub struct ExplorationReportJson {
352 pub total_runs: usize,
354 pub unique_classes: usize,
356 pub violations: Vec<ViolationSummary>,
358 pub violation_seeds: Vec<u64>,
360 pub coverage: CoverageMetrics,
362 pub top_unexplored: Vec<UnexploredSeedJson>,
364 pub runs: Vec<RunSummary>,
366 pub certificate_divergences: Vec<(u64, u64)>,
368}
369
370#[derive(Debug, Serialize)]
372pub struct RunSummary {
373 pub seed: u64,
375 pub steps: u64,
377 pub fingerprint: u64,
379 pub is_new_class: bool,
381 pub violation_count: usize,
383 pub certificate_hash: u64,
385}
386
387impl RunResult {
388 fn summary(&self) -> RunSummary {
389 RunSummary {
390 seed: self.seed,
391 steps: self.steps,
392 fingerprint: self.fingerprint,
393 is_new_class: self.is_new_class,
394 violation_count: self.violations.len(),
395 certificate_hash: self.certificate_hash,
396 }
397 }
398}
399
400#[derive(Debug, Serialize)]
402pub struct ViolationSummary {
403 pub seed: u64,
405 pub steps: u64,
407 pub fingerprint: u64,
409 pub violations: Vec<String>,
411}
412
413impl ViolationReport {
414 fn summary(&self) -> ViolationSummary {
415 ViolationSummary {
416 seed: self.seed,
417 steps: self.steps,
418 fingerprint: self.fingerprint,
419 violations: self.violations.iter().map(ToString::to_string).collect(),
420 }
421 }
422}
423
424#[derive(Debug, Serialize)]
426pub struct TopologicalScoreJson {
427 pub novelty: u32,
429 pub persistence_sum: u64,
431 pub fingerprint: u64,
433}
434
435impl From<TopologicalScore> for TopologicalScoreJson {
436 fn from(score: TopologicalScore) -> Self {
437 Self {
438 novelty: score.novelty,
439 persistence_sum: score.persistence_sum,
440 fingerprint: score.fingerprint,
441 }
442 }
443}
444
445#[derive(Debug, Serialize)]
447pub struct UnexploredSeedJson {
448 pub seed: u64,
450 pub score: Option<TopologicalScoreJson>,
452}
453
454impl UnexploredSeedJson {
455 fn from_seed(seed: &UnexploredSeed) -> Self {
456 Self {
457 seed: seed.seed,
458 score: seed.score.map(TopologicalScoreJson::from),
459 }
460 }
461}
462
463pub struct ScheduleExplorer {
468 config: ExplorerConfig,
469 explored_seeds: HashSet<u64>,
470 known_fingerprints: HashSet<u64>,
471 class_counts: HashMap<u64, usize>,
472 results: Vec<RunResult>,
473 violations: Vec<ViolationReport>,
474 new_class_count: usize,
475}
476
477impl ScheduleExplorer {
478 #[must_use]
480 pub fn new(config: ExplorerConfig) -> Self {
481 Self {
482 config,
483 explored_seeds: HashSet::new(),
484 known_fingerprints: HashSet::new(),
485 class_counts: HashMap::new(),
486 results: Vec::new(),
487 violations: Vec::new(),
488 new_class_count: 0,
489 }
490 }
491
492 pub fn explore<F>(&mut self, test: F) -> ExplorationReport
515 where
516 F: Fn(&mut LabRuntime),
517 {
518 for run_idx in 0..self.config.max_runs {
519 let seed = self.config.base_seed.wrapping_add(run_idx as u64);
520 self.run_once(seed, &test);
521 }
522
523 self.build_report()
524 }
525
526 fn run_once<F>(&mut self, seed: u64, test: &F)
528 where
529 F: Fn(&mut LabRuntime),
530 {
531 if !self.explored_seeds.insert(seed) {
532 return;
533 }
534
535 let mut lab_config = LabConfig::new(seed);
537 lab_config = lab_config.worker_count(self.config.worker_count);
538 if let Some(max) = Some(self.config.max_steps_per_run) {
539 lab_config = lab_config.max_steps(max);
540 }
541 if self.config.record_traces {
542 lab_config = lab_config.with_default_replay_recording();
543 }
544
545 let mut runtime = LabRuntime::new(lab_config);
546
547 test(&mut runtime);
549
550 let steps = runtime.steps();
551
552 let trace_events: Vec<TraceEvent> = runtime.trace().snapshot();
554 let fingerprint = if trace_events.is_empty() {
555 seed
557 } else {
558 trace_fingerprint(&trace_events)
559 };
560
561 let is_new_class = self.known_fingerprints.insert(fingerprint);
562 if is_new_class {
563 self.new_class_count += 1;
564 }
565 *self.class_counts.entry(fingerprint).or_insert(0) += 1;
566
567 let violations = runtime.check_invariants();
569
570 if !violations.is_empty() {
571 self.violations.push(ViolationReport {
572 seed,
573 steps,
574 violations: violations.clone(),
575 fingerprint,
576 });
577 }
578
579 let certificate_hash = runtime.certificate().hash();
580
581 self.results.push(RunResult {
582 seed,
583 steps,
584 fingerprint,
585 is_new_class,
586 violations,
587 certificate_hash,
588 });
589 }
590
591 fn build_report(&self) -> ExplorationReport {
593 let total_runs = self.results.len();
594 let novelty_histogram = novelty_histogram_from_flags(&self.results);
595 let saturation = saturation_metrics(&self.results, total_runs, self.new_class_count);
596 ExplorationReport {
597 total_runs,
598 unique_classes: self.known_fingerprints.len(),
599 violations: self.violations.clone(),
600 coverage: CoverageMetrics {
601 equivalence_classes: self.known_fingerprints.len(),
602 total_runs,
603 new_class_discoveries: self.new_class_count,
604 class_run_counts: self.class_counts.clone(),
605 novelty_histogram,
606 saturation,
607 },
608 top_unexplored: Vec::new(),
609 runs: Vec::new(), }
611 }
612
613 #[must_use]
615 pub fn results(&self) -> &[RunResult] {
616 &self.results
617 }
618
619 #[must_use]
621 pub fn coverage(&self) -> CoverageMetrics {
622 let total_runs = self.results.len();
623 let novelty_histogram = novelty_histogram_from_flags(&self.results);
624 let saturation = saturation_metrics(&self.results, total_runs, self.new_class_count);
625 CoverageMetrics {
626 equivalence_classes: self.known_fingerprints.len(),
627 total_runs,
628 new_class_discoveries: self.new_class_count,
629 class_run_counts: self.class_counts.clone(),
630 novelty_histogram,
631 saturation,
632 }
633 }
634}
635
636pub struct DporExplorer {
660 config: ExplorerConfig,
661 work_queue: VecDeque<u64>,
663 explored_seeds: HashSet<u64>,
665 known_classes: HashMap<u64, TraceMonoid>,
667 class_counts: HashMap<u64, usize>,
669 results: Vec<RunResult>,
671 violations: Vec<ViolationReport>,
673 total_races: usize,
675 total_hb_races: usize,
677 total_backtrack_points: usize,
679 pruned_backtrack_points: usize,
681 sleep_pruned: usize,
683 sleep_set: crate::trace::dpor::SleepSet,
685 per_run_estimated_classes: Vec<usize>,
687}
688
689#[derive(Debug, Clone, Serialize)]
691pub struct DporCoverageMetrics {
692 pub base: CoverageMetrics,
694 pub total_races: usize,
696 pub total_hb_races: usize,
698 pub total_backtrack_points: usize,
700 pub pruned_backtrack_points: usize,
702 pub sleep_pruned: usize,
704 pub efficiency: f64,
706 pub estimated_class_trend: Vec<usize>,
708}
709
710impl DporExplorer {
711 #[must_use]
713 pub fn new(config: ExplorerConfig) -> Self {
714 let mut work_queue = VecDeque::new();
715 work_queue.push_back(config.base_seed);
716 Self {
717 config,
718 work_queue,
719 explored_seeds: HashSet::new(),
720 known_classes: HashMap::new(),
721 class_counts: HashMap::new(),
722 results: Vec::new(),
723 violations: Vec::new(),
724 total_races: 0,
725 total_hb_races: 0,
726 total_backtrack_points: 0,
727 pruned_backtrack_points: 0,
728 sleep_pruned: 0,
729 sleep_set: crate::trace::dpor::SleepSet::new(),
730 per_run_estimated_classes: Vec::new(),
731 }
732 }
733
734 pub fn explore<F>(&mut self, test: F) -> ExplorationReport
740 where
741 F: Fn(&mut LabRuntime),
742 {
743 while let Some(seed) = self.work_queue.pop_front() {
744 if self.results.len() >= self.config.max_runs {
745 break;
746 }
747 if !self.explored_seeds.insert(seed) {
748 continue;
749 }
750
751 let (trace_events, run_result) = self.run_once(seed, &test);
752
753 if trace_events.is_empty() {
755 self.per_run_estimated_classes.push(1);
756 } else {
757 let analysis = detect_races(&trace_events);
758 self.total_races += analysis.race_count();
759 self.total_backtrack_points += analysis.backtrack_points.len();
760
761 let hb_report = crate::trace::dpor::detect_hb_races(&trace_events);
763 self.total_hb_races += hb_report.race_count();
764
765 let est = crate::trace::dpor::estimated_classes(&trace_events);
767 self.per_run_estimated_classes.push(est);
768
769 for bp in &analysis.backtrack_points {
774 if self.sleep_set.contains(bp, &trace_events) {
777 self.sleep_pruned += 1;
778 continue;
779 }
780 self.sleep_set.insert(bp, &trace_events);
781
782 let derived_seed =
783 seed ^ (bp.divergence_index as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
784
785 if self.explored_seeds.contains(&derived_seed) {
786 self.pruned_backtrack_points += 1;
787 continue;
788 }
789
790 let prefix = &trace_events[..bp.divergence_index.min(trace_events.len())];
794 let prefix_fp = trace_fingerprint(prefix);
795 if self.known_classes.contains_key(&prefix_fp) && prefix.len() > 1 {
796 self.work_queue.push_back(derived_seed);
799 } else {
800 self.work_queue.push_front(derived_seed);
802 }
803 }
804 }
805
806 self.results.push(run_result);
807 }
808
809 self.build_report()
810 }
811
812 fn run_once<F>(&mut self, seed: u64, test: &F) -> (Vec<TraceEvent>, RunResult)
814 where
815 F: Fn(&mut LabRuntime),
816 {
817 let mut lab_config = LabConfig::new(seed);
818 lab_config = lab_config.worker_count(self.config.worker_count);
819 if let Some(max) = Some(self.config.max_steps_per_run) {
820 lab_config = lab_config.max_steps(max);
821 }
822 if self.config.record_traces {
823 lab_config = lab_config.with_default_replay_recording();
824 }
825
826 let mut runtime = LabRuntime::new(lab_config);
827 test(&mut runtime);
828
829 let steps = runtime.steps();
830 let trace_events: Vec<TraceEvent> = runtime.trace().snapshot();
831
832 let monoid = TraceMonoid::from_events(&trace_events);
833 let fingerprint = monoid.class_fingerprint();
834
835 let is_new_class = !self.known_classes.contains_key(&fingerprint);
836 if is_new_class {
837 self.known_classes.insert(fingerprint, monoid);
838 }
839 *self.class_counts.entry(fingerprint).or_insert(0) += 1;
840
841 let violations = runtime.check_invariants();
842 if !violations.is_empty() {
843 self.violations.push(ViolationReport {
844 seed,
845 steps,
846 violations: violations.clone(),
847 fingerprint,
848 });
849 }
850
851 let certificate_hash = runtime.certificate().hash();
852
853 let result = RunResult {
854 seed,
855 steps,
856 fingerprint,
857 is_new_class,
858 violations,
859 certificate_hash,
860 };
861
862 (trace_events, result)
863 }
864
865 fn build_report(&self) -> ExplorationReport {
866 let total_runs = self.results.len();
867 let new_class_discoveries = self.results.iter().filter(|r| r.is_new_class).count();
868 let novelty_histogram = novelty_histogram_from_flags(&self.results);
869 let saturation = saturation_metrics(&self.results, total_runs, new_class_discoveries);
870 let top_unexplored = self
871 .work_queue
872 .iter()
873 .take(DEFAULT_UNEXPLORED_LIMIT)
874 .map(|seed| UnexploredSeed {
875 seed: *seed,
876 score: None,
877 })
878 .collect();
879 ExplorationReport {
880 total_runs,
881 unique_classes: self.known_classes.len(),
882 violations: self.violations.clone(),
883 coverage: CoverageMetrics {
884 equivalence_classes: self.known_classes.len(),
885 total_runs,
886 new_class_discoveries,
887 class_run_counts: self.class_counts.clone(),
888 novelty_histogram,
889 saturation,
890 },
891 top_unexplored,
892 runs: Vec::new(),
893 }
894 }
895
896 #[must_use]
898 #[allow(clippy::cast_precision_loss)]
899 pub fn dpor_coverage(&self) -> DporCoverageMetrics {
900 let new_class_count = self.results.iter().filter(|r| r.is_new_class).count();
901 let total = self.results.len();
902 let novelty_histogram = novelty_histogram_from_flags(&self.results);
903 let saturation = saturation_metrics(&self.results, total, new_class_count);
904 DporCoverageMetrics {
905 base: CoverageMetrics {
906 equivalence_classes: self.known_classes.len(),
907 total_runs: total,
908 new_class_discoveries: new_class_count,
909 class_run_counts: self.class_counts.clone(),
910 novelty_histogram,
911 saturation,
912 },
913 total_races: self.total_races,
914 total_hb_races: self.total_hb_races,
915 total_backtrack_points: self.total_backtrack_points,
916 pruned_backtrack_points: self.pruned_backtrack_points,
917 sleep_pruned: self.sleep_pruned,
918 efficiency: if total == 0 {
919 0.0
920 } else {
921 new_class_count as f64 / total as f64
922 },
923 estimated_class_trend: self.per_run_estimated_classes.clone(),
924 }
925 }
926}
927
928pub struct TopologyExplorer {
947 config: ExplorerConfig,
948 frontier: BinaryHeap<(TopologicalScore, u64)>,
950 explored_seeds: HashSet<u64>,
952 known_fingerprints: HashSet<u64>,
954 class_counts: HashMap<u64, usize>,
955 seen_classes: HashSet<ClassId>,
957 results: Vec<RunResult>,
959 violations: Vec<ViolationReport>,
961 ledgers: Vec<EvidenceLedger>,
963 new_class_count: usize,
964}
965
966impl TopologyExplorer {
967 #[must_use]
969 pub fn new(config: ExplorerConfig) -> Self {
970 let mut frontier = BinaryHeap::new();
971 for i in 0..config.max_runs {
973 let seed = config.base_seed.wrapping_add(i as u64);
974 let fp = seed_fingerprint(seed);
975 frontier.push((TopologicalScore::zero(fp), seed));
976 }
977 Self {
978 config,
979 frontier,
980 explored_seeds: HashSet::new(),
981 known_fingerprints: HashSet::new(),
982 class_counts: HashMap::new(),
983 seen_classes: HashSet::new(),
984 results: Vec::new(),
985 violations: Vec::new(),
986 ledgers: Vec::new(),
987 new_class_count: 0,
988 }
989 }
990
991 pub fn explore<F>(&mut self, test: F) -> ExplorationReport
995 where
996 F: Fn(&mut LabRuntime),
997 {
998 while let Some((_score, seed)) = self.frontier.pop() {
999 if self.results.len() >= self.config.max_runs {
1000 break;
1001 }
1002 if !self.explored_seeds.insert(seed) {
1003 continue;
1004 }
1005 self.run_once(seed, &test);
1006 }
1007 self.build_report()
1008 }
1009
1010 fn run_once<F>(&mut self, seed: u64, test: &F)
1011 where
1012 F: Fn(&mut LabRuntime),
1013 {
1014 let mut lab_config = LabConfig::new(seed);
1015 lab_config = lab_config.worker_count(self.config.worker_count);
1016 if let Some(max) = Some(self.config.max_steps_per_run) {
1017 lab_config = lab_config.max_steps(max);
1018 }
1019 if self.config.record_traces {
1020 lab_config = lab_config.with_default_replay_recording();
1021 }
1022
1023 let mut runtime = LabRuntime::new(lab_config);
1024 test(&mut runtime);
1025
1026 let steps = runtime.steps();
1027 let trace_events: Vec<TraceEvent> = runtime.trace().snapshot();
1028
1029 let fingerprint = if trace_events.is_empty() {
1030 seed
1031 } else {
1032 trace_fingerprint(&trace_events)
1033 };
1034
1035 let is_new_class = self.known_fingerprints.insert(fingerprint);
1036 if is_new_class {
1037 self.new_class_count += 1;
1038 }
1039 *self.class_counts.entry(fingerprint).or_insert(0) += 1;
1040
1041 let fp = seed_fingerprint(seed);
1043 let ledger = if trace_events.len() >= 2 {
1044 let poset = TracePoset::from_trace(&trace_events);
1045 let complex = SquareComplex::from_trace_poset(&poset);
1046 let d2 = complex.boundary_2();
1047
1048 let reduced = d2.reduce();
1056 let pairs = reduced.persistence_pairs();
1057 score_persistence(&pairs, &mut self.seen_classes, fp)
1058 } else {
1059 use crate::trace::gf2::PersistencePairs;
1060 let pairs = PersistencePairs {
1061 pairs: vec![],
1062 unpaired: vec![],
1063 };
1064 score_persistence(&pairs, &mut self.seen_classes, fp)
1065 };
1066
1067 self.enqueue_derived_seeds(seed, &ledger);
1068 self.ledgers.push(ledger);
1069
1070 let violations = runtime.check_invariants();
1071 if !violations.is_empty() {
1072 self.violations.push(ViolationReport {
1073 seed,
1074 steps,
1075 violations: violations.clone(),
1076 fingerprint,
1077 });
1078 }
1079
1080 let certificate_hash = runtime.certificate().hash();
1081
1082 self.results.push(RunResult {
1083 seed,
1084 steps,
1085 fingerprint,
1086 is_new_class,
1087 violations,
1088 certificate_hash,
1089 });
1090 }
1091
1092 fn enqueue_derived_seeds(&mut self, seed: u64, ledger: &EvidenceLedger) {
1093 if ledger.entries.is_empty() {
1094 return;
1095 }
1096 if ledger.score.novelty == 0 && ledger.score.persistence_sum == 0 {
1097 return;
1098 }
1099 let mut pushed = 0usize;
1100 for (idx, entry) in ledger.entries.iter().enumerate() {
1101 if pushed >= DEFAULT_DERIVED_SEEDS {
1102 break;
1103 }
1104 let derived_seed = derive_seed(seed, entry.class, idx as u64);
1105 if self.explored_seeds.contains(&derived_seed) {
1106 continue;
1107 }
1108 let mut score = ledger.score;
1109 score.fingerprint = seed_fingerprint(derived_seed);
1110 self.frontier.push((score, derived_seed));
1111 pushed += 1;
1112 }
1113 }
1114
1115 fn build_report(&self) -> ExplorationReport {
1116 let total_runs = self.results.len();
1117 let novelty_histogram = novelty_histogram_from_ledgers(&self.ledgers);
1118 let saturation = saturation_metrics(&self.results, total_runs, self.new_class_count);
1119 ExplorationReport {
1120 total_runs,
1121 unique_classes: self.known_fingerprints.len(),
1122 violations: self.violations.clone(),
1123 coverage: CoverageMetrics {
1124 equivalence_classes: self.known_fingerprints.len(),
1125 total_runs,
1126 new_class_discoveries: self.new_class_count,
1127 class_run_counts: self.class_counts.clone(),
1128 novelty_histogram,
1129 saturation,
1130 },
1131 top_unexplored: self.top_unexplored(DEFAULT_UNEXPLORED_LIMIT),
1132 runs: Vec::new(),
1133 }
1134 }
1135
1136 #[must_use]
1138 pub fn results(&self) -> &[RunResult] {
1139 &self.results
1140 }
1141
1142 #[must_use]
1144 pub fn ledgers(&self) -> &[EvidenceLedger] {
1145 &self.ledgers
1146 }
1147
1148 #[must_use]
1150 pub fn coverage(&self) -> CoverageMetrics {
1151 let total_runs = self.results.len();
1152 let novelty_histogram = novelty_histogram_from_ledgers(&self.ledgers);
1153 let saturation = saturation_metrics(&self.results, total_runs, self.new_class_count);
1154 CoverageMetrics {
1155 equivalence_classes: self.known_fingerprints.len(),
1156 total_runs,
1157 new_class_discoveries: self.new_class_count,
1158 class_run_counts: self.class_counts.clone(),
1159 novelty_histogram,
1160 saturation,
1161 }
1162 }
1163
1164 fn top_unexplored(&self, limit: usize) -> Vec<UnexploredSeed> {
1165 let mut heap = self.frontier.clone();
1166 let mut out = Vec::new();
1167 while out.len() < limit {
1168 let Some((score, seed)) = heap.pop() else {
1169 break;
1170 };
1171 out.push(UnexploredSeed {
1172 seed,
1173 score: Some(score),
1174 });
1175 }
1176 out
1177 }
1178}
1179
1180fn derive_seed(seed: u64, class: ClassId, index: u64) -> u64 {
1181 let mut hasher = DefaultHasher::new();
1182 seed.hash(&mut hasher);
1183 class.birth.hash(&mut hasher);
1184 class.death.hash(&mut hasher);
1185 index.hash(&mut hasher);
1186 hasher.finish()
1187}
1188
1189impl Clone for ViolationReport {
1191 fn clone(&self) -> Self {
1192 Self {
1193 seed: self.seed,
1194 steps: self.steps,
1195 violations: self.violations.clone(),
1196 fingerprint: self.fingerprint,
1197 }
1198 }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use super::*;
1204 use crate::types::Budget;
1205 use serde_json::Value as JsonValue;
1206 use std::fs;
1207 use tempfile::NamedTempFile;
1208
1209 #[test]
1210 fn explore_single_task_no_violations() {
1211 let mut explorer = ScheduleExplorer::new(ExplorerConfig::new(42, 5));
1212 let report = explorer.explore(|runtime| {
1213 let region = runtime.state.create_root_region(Budget::INFINITE);
1214 let (task_id, _handle) = runtime
1215 .state
1216 .create_task(region, Budget::INFINITE, async { 42 })
1217 .expect("create task");
1218 runtime.scheduler.lock().unwrap().schedule(task_id, 0);
1219 runtime.run_until_quiescent();
1220 });
1221
1222 assert!(!report.has_violations());
1223 assert_eq!(report.total_runs, 5);
1224 assert!(report.unique_classes >= 1);
1229 }
1230
1231 #[test]
1232 fn explore_two_independent_tasks_discovers_classes() {
1233 let mut explorer = ScheduleExplorer::new(ExplorerConfig::new(0, 20));
1234 let report = explorer.explore(|runtime| {
1235 let region = runtime.state.create_root_region(Budget::INFINITE);
1236 let (t1, _) = runtime
1237 .state
1238 .create_task(region, Budget::INFINITE, async {})
1239 .expect("t1");
1240 let (t2, _) = runtime
1241 .state
1242 .create_task(region, Budget::INFINITE, async {})
1243 .expect("t2");
1244 {
1245 let mut sched = runtime.scheduler.lock().unwrap();
1246 sched.schedule(t1, 0);
1247 sched.schedule(t2, 0);
1248 }
1249 runtime.run_until_quiescent();
1250 });
1251
1252 assert!(!report.has_violations());
1253 assert_eq!(report.total_runs, 20);
1254 assert!(report.unique_classes >= 1);
1258 }
1259
1260 #[test]
1261 fn coverage_metrics_track_discovery() {
1262 let mut explorer = ScheduleExplorer::new(ExplorerConfig::new(100, 10));
1263 let report = explorer.explore(|runtime| {
1264 let region = runtime.state.create_root_region(Budget::INFINITE);
1265 let (t1, _) = runtime
1266 .state
1267 .create_task(region, Budget::INFINITE, async {})
1268 .expect("t1");
1269 runtime.scheduler.lock().unwrap().schedule(t1, 0);
1270 runtime.run_until_quiescent();
1271 });
1272
1273 let cov = &report.coverage;
1274 assert_eq!(cov.total_runs, 10);
1275 assert!(cov.equivalence_classes >= 1);
1276 assert!(cov.new_class_discoveries >= 1);
1277 assert!(cov.discovery_rate() > 0.0);
1279 assert!(cov.discovery_rate() <= 1.0);
1280 let hist_total: usize = cov.novelty_histogram.values().copied().sum();
1281 assert_eq!(hist_total, cov.total_runs);
1282 assert_eq!(cov.saturation.window, DEFAULT_SATURATION_WINDOW);
1283 }
1284
1285 #[test]
1286 fn violation_seeds_are_recorded() {
1287 let mut explorer = ScheduleExplorer::new(ExplorerConfig::new(42, 3));
1290 let report = explorer.explore(|runtime| {
1291 let _region = runtime.state.create_root_region(Budget::INFINITE);
1292 runtime.run_until_quiescent();
1293 });
1294
1295 assert!(report.violation_seeds().is_empty());
1297 }
1298
1299 #[test]
1300 fn explorer_config_builder() {
1301 let config = ExplorerConfig::new(42, 50)
1302 .worker_count(4)
1303 .max_steps(10_000);
1304 assert_eq!(config.base_seed, 42);
1305 assert_eq!(config.max_runs, 50);
1306 assert_eq!(config.worker_count, 4);
1307 assert_eq!(config.max_steps_per_run, 10_000);
1308 }
1309
1310 #[test]
1311 fn discovery_rate_correct() {
1312 let mut novelty_histogram = BTreeMap::new();
1313 novelty_histogram.insert(0, 7);
1314 novelty_histogram.insert(1, 3);
1315 let saturation = SaturationMetrics {
1316 window: DEFAULT_SATURATION_WINDOW,
1317 saturated: false,
1318 existing_class_hits: 7,
1319 runs_since_last_new_class: Some(7),
1320 };
1321 let metrics = CoverageMetrics {
1322 equivalence_classes: 3,
1323 total_runs: 10,
1324 new_class_discoveries: 3,
1325 class_run_counts: HashMap::new(),
1326 novelty_histogram,
1327 saturation,
1328 };
1329 assert!((metrics.discovery_rate() - 0.3).abs() < 1e-10);
1330 }
1331
1332 #[test]
1335 fn dpor_explore_single_task_no_violations() {
1336 let mut explorer = DporExplorer::new(ExplorerConfig::new(42, 10));
1337 let report = explorer.explore(|runtime| {
1338 let region = runtime.state.create_root_region(Budget::INFINITE);
1339 let (task_id, _handle) = runtime
1340 .state
1341 .create_task(region, Budget::INFINITE, async { 42 })
1342 .expect("create task");
1343 runtime.scheduler.lock().unwrap().schedule(task_id, 0);
1344 runtime.run_until_quiescent();
1345 });
1346
1347 assert!(!report.has_violations());
1348 assert!(report.unique_classes >= 1);
1349 }
1350
1351 #[test]
1352 fn dpor_explore_two_tasks_discovers_classes() {
1353 let mut explorer = DporExplorer::new(ExplorerConfig::new(0, 20));
1354 let report = explorer.explore(|runtime| {
1355 let region = runtime.state.create_root_region(Budget::INFINITE);
1356 let (t1, _) = runtime
1357 .state
1358 .create_task(region, Budget::INFINITE, async {})
1359 .expect("t1");
1360 let (t2, _) = runtime
1361 .state
1362 .create_task(region, Budget::INFINITE, async {})
1363 .expect("t2");
1364 {
1365 let mut sched = runtime.scheduler.lock().unwrap();
1366 sched.schedule(t1, 0);
1367 sched.schedule(t2, 0);
1368 }
1369 runtime.run_until_quiescent();
1370 });
1371
1372 assert!(!report.has_violations());
1373 assert!(report.unique_classes >= 1);
1374 }
1375
1376 #[test]
1377 fn dpor_coverage_metrics_populated() {
1378 let mut explorer = DporExplorer::new(ExplorerConfig::new(42, 5));
1379 let _report = explorer.explore(|runtime| {
1380 let region = runtime.state.create_root_region(Budget::INFINITE);
1381 let (t1, _) = runtime
1382 .state
1383 .create_task(region, Budget::INFINITE, async {})
1384 .expect("t1");
1385 runtime.scheduler.lock().unwrap().schedule(t1, 0);
1386 runtime.run_until_quiescent();
1387 });
1388
1389 let metrics = explorer.dpor_coverage();
1390 assert!(metrics.base.total_runs >= 1);
1391 assert!(metrics.base.equivalence_classes >= 1);
1392 assert!(metrics.efficiency >= 0.0);
1394 assert!(metrics.efficiency <= 1.0);
1395 }
1396
1397 #[test]
1398 fn dpor_explorer_respects_max_runs() {
1399 let mut explorer = DporExplorer::new(ExplorerConfig::new(0, 3));
1400 let report = explorer.explore(|runtime| {
1401 let _region = runtime.state.create_root_region(Budget::INFINITE);
1402 runtime.run_until_quiescent();
1403 });
1404
1405 assert!(report.total_runs <= 3);
1406 }
1407
1408 #[test]
1411 fn certificate_hash_populated_in_run_results() {
1412 let mut explorer = ScheduleExplorer::new(ExplorerConfig::new(42, 3));
1413 let report = explorer.explore(|runtime| {
1414 let region = runtime.state.create_root_region(Budget::INFINITE);
1415 let (t, _) = runtime
1416 .state
1417 .create_task(region, Budget::INFINITE, async { 1 })
1418 .expect("t");
1419 runtime.scheduler.lock().unwrap().schedule(t, 0);
1420 runtime.run_until_quiescent();
1421 });
1422
1423 for r in &report.runs {
1425 assert_ne!(r.certificate_hash, 0, "seed {} had zero cert hash", r.seed);
1426 }
1427 }
1428
1429 #[test]
1430 fn same_seed_produces_same_certificate() {
1431 let run = |seed: u64| -> u64 {
1432 let mut explorer = ScheduleExplorer::new(ExplorerConfig::new(seed, 1));
1433 explorer.explore(|runtime| {
1434 let region = runtime.state.create_root_region(Budget::INFINITE);
1435 let (t, _) = runtime
1436 .state
1437 .create_task(region, Budget::INFINITE, async { 99 })
1438 .expect("t");
1439 runtime.scheduler.lock().unwrap().schedule(t, 0);
1440 runtime.run_until_quiescent();
1441 });
1442 let first = explorer
1443 .results()
1444 .first()
1445 .expect("explorer should record at least one run");
1446 first.certificate_hash
1447 };
1448
1449 let h1 = run(77);
1450 let h2 = run(77);
1451 assert_eq!(h1, h2, "same seed should yield same certificate");
1452 }
1453
1454 #[test]
1455 fn different_seeds_may_produce_different_certificates() {
1456 let run = |seed: u64| -> u64 {
1457 let mut explorer = ScheduleExplorer::new(ExplorerConfig::new(seed, 1));
1458 explorer.explore(|runtime| {
1459 let region = runtime.state.create_root_region(Budget::INFINITE);
1460 let (t1, _) = runtime
1461 .state
1462 .create_task(region, Budget::INFINITE, async {})
1463 .expect("t1");
1464 let (t2, _) = runtime
1465 .state
1466 .create_task(region, Budget::INFINITE, async {})
1467 .expect("t2");
1468 {
1469 let mut sched = runtime.scheduler.lock().unwrap();
1470 sched.schedule(t1, 0);
1471 sched.schedule(t2, 0);
1472 }
1473 runtime.run_until_quiescent();
1474 });
1475 let first = explorer
1476 .results()
1477 .first()
1478 .expect("explorer should record at least one run");
1479 first.certificate_hash
1480 };
1481
1482 let hashes: HashSet<u64> = (0..10).map(run).collect();
1485 assert!(!hashes.is_empty());
1486 }
1487
1488 #[test]
1489 fn certificates_consistent_with_single_task() {
1490 let mut explorer = ScheduleExplorer::new(ExplorerConfig::new(0, 5));
1491 let report = explorer.explore(|runtime| {
1492 let region = runtime.state.create_root_region(Budget::INFINITE);
1493 let (t, _) = runtime
1494 .state
1495 .create_task(region, Budget::INFINITE, async { 42 })
1496 .expect("t");
1497 runtime.scheduler.lock().unwrap().schedule(t, 0);
1498 runtime.run_until_quiescent();
1499 });
1500
1501 assert!(report.certificates_consistent());
1504 }
1505
1506 #[test]
1507 fn dpor_certificate_hash_populated() {
1508 let mut explorer = DporExplorer::new(ExplorerConfig::new(42, 5));
1509 let report = explorer.explore(|runtime| {
1510 let region = runtime.state.create_root_region(Budget::INFINITE);
1511 let (t, _) = runtime
1512 .state
1513 .create_task(region, Budget::INFINITE, async { 1 })
1514 .expect("t");
1515 runtime.scheduler.lock().unwrap().schedule(t, 0);
1516 runtime.run_until_quiescent();
1517 });
1518
1519 for r in &report.runs {
1520 assert_ne!(r.certificate_hash, 0, "seed {} had zero cert hash", r.seed);
1521 }
1522 }
1523
1524 #[test]
1525 fn json_summary_includes_core_fields() {
1526 let mut explorer = ScheduleExplorer::new(ExplorerConfig::new(7, 2));
1527 let report = explorer.explore(|runtime| {
1528 let region = runtime.state.create_root_region(Budget::INFINITE);
1529 let (task_id, _handle) = runtime
1530 .state
1531 .create_task(region, Budget::INFINITE, async {})
1532 .expect("create task");
1533 runtime.scheduler.lock().unwrap().schedule(task_id, 0);
1534 runtime.run_until_quiescent();
1535 });
1536
1537 let json = report.to_json_string().expect("json");
1538 let value: JsonValue = serde_json::from_str(&json).expect("parse");
1539 assert!(value.get("total_runs").is_some());
1540 assert!(value.get("unique_classes").is_some());
1541 assert!(value.get("coverage").is_some());
1542 assert!(value.get("violations").is_some());
1543 assert!(value.get("violation_seeds").is_some());
1544 }
1545
1546 #[test]
1547 fn json_summary_can_be_written() {
1548 let mut explorer = ScheduleExplorer::new(ExplorerConfig::new(11, 1));
1549 let report = explorer.explore(|runtime| {
1550 let _region = runtime.state.create_root_region(Budget::INFINITE);
1551 runtime.run_until_quiescent();
1552 });
1553
1554 let tmp = NamedTempFile::new().expect("tmp");
1555 report
1556 .write_json_summary(tmp.path(), false)
1557 .expect("write json");
1558 let contents = fs::read_to_string(tmp.path()).expect("read");
1559 let value: JsonValue = serde_json::from_str(&contents).expect("parse");
1560 assert!(value.get("coverage").is_some());
1561 }
1562}