1#![forbid(unsafe_code)]
20
21use serde::{Deserialize, Serialize};
22use std::cell::{Cell, RefCell};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum TieBreakPolicy {
38 LexMin,
40 LexMax,
42 InsertionOrder,
44 ReverseInsertionOrder,
46 WeightThenLex,
48 WeightThenInsertionOrder,
56 LexThenWeight,
58 DeterministicHash { seed: u64 },
60 DegreeMinThenLex,
62 DegreeMaxThenLex,
64 DfsPreorder,
66 BfsLevelLex,
68 EdgeKeyLex,
70}
71
72impl TieBreakPolicy {
73 #[must_use]
75 pub fn id(&self) -> &'static str {
76 match self {
77 Self::LexMin => "lex_min",
78 Self::LexMax => "lex_max",
79 Self::InsertionOrder => "insertion_order",
80 Self::ReverseInsertionOrder => "reverse_insertion_order",
81 Self::WeightThenLex => "weight_then_lex",
82 Self::WeightThenInsertionOrder => "weight_then_insertion_order",
83 Self::LexThenWeight => "lex_then_weight",
84 Self::DeterministicHash { .. } => "deterministic_hash",
85 Self::DegreeMinThenLex => "degree_min_then_lex",
86 Self::DegreeMaxThenLex => "degree_max_then_lex",
87 Self::DfsPreorder => "dfs_preorder",
88 Self::BfsLevelLex => "bfs_level_lex",
89 Self::EdgeKeyLex => "edge_key_lex",
90 }
91 }
92
93 pub fn sort_candidates<T: AsRef<str>>(&self, candidates: &mut [T]) {
95 match self {
96 Self::LexMin => candidates.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref())),
97 Self::LexMax => {
98 candidates.sort_unstable_by(|a, b| b.as_ref().cmp(a.as_ref()));
99 }
100 Self::InsertionOrder | Self::ReverseInsertionOrder => {
101 }
104 Self::WeightThenLex | Self::WeightThenInsertionOrder | Self::LexThenWeight => {
105 }
112 Self::DeterministicHash { seed } => {
113 let s = *seed;
114 candidates.sort_unstable_by_key(|label| {
115 let mut hasher = blake3::Hasher::new();
116 hasher.update(&s.to_le_bytes());
117 hasher.update(label.as_ref().as_bytes());
118 *hasher.finalize().as_bytes()
119 });
120 }
121 Self::DegreeMinThenLex | Self::DegreeMaxThenLex => {
122 }
124 Self::DfsPreorder | Self::BfsLevelLex | Self::EdgeKeyLex => {
125 }
127 }
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ComplexityWitness {
143 pub n: usize,
145 pub m: usize,
147 pub dominant_term: String,
149 pub observed_count: u64,
151 pub policy: TieBreakPolicy,
153 pub seed: Option<u64>,
155 pub decision_path_blake3: [u8; 32],
157}
158
159pub struct WitnessSink {
170 hasher: blake3::Hasher,
171 count: u64,
172 policy: TieBreakPolicy,
173}
174
175impl WitnessSink {
176 #[must_use]
178 pub fn new(policy: TieBreakPolicy) -> Self {
179 Self {
180 hasher: blake3::Hasher::new(),
181 count: 0,
182 policy,
183 }
184 }
185
186 pub fn record_decision(&mut self, chosen: &str, rejected: &str) {
189 self.record_len_prefixed(chosen.as_bytes());
190 self.record_len_prefixed(rejected.as_bytes());
191 self.count += 1;
192 }
193
194 pub fn record_decision_bytes(&mut self, chosen: &[u8], rejected: &[u8]) {
196 self.record_len_prefixed(chosen);
197 self.record_len_prefixed(rejected);
198 self.count += 1;
199 }
200
201 fn record_len_prefixed(&mut self, bytes: &[u8]) {
202 let len = bytes.len() as u64;
203 self.hasher.update(&len.to_le_bytes());
204 self.hasher.update(bytes);
205 }
206
207 #[must_use]
209 pub fn finalize(
210 self,
211 n: usize,
212 m: usize,
213 dominant_term: &str,
214 seed: Option<u64>,
215 ) -> ComplexityWitness {
216 let hash = *self.hasher.finalize().as_bytes();
217 ComplexityWitness {
218 n,
219 m,
220 dominant_term: dominant_term.to_string(),
221 observed_count: self.count,
222 policy: self.policy,
223 seed,
224 decision_path_blake3: hash,
225 }
226 }
227}
228
229#[derive(Debug, Default, Serialize, Deserialize)]
238pub struct WitnessLedger {
239 pub entries: Vec<ComplexityWitness>,
240}
241
242impl WitnessLedger {
243 #[must_use]
244 pub fn new() -> Self {
245 Self {
246 entries: Vec::new(),
247 }
248 }
249
250 pub fn append(&mut self, witness: ComplexityWitness) {
251 self.entries.push(witness);
252 }
253
254 pub fn to_jsonl(&self) -> String {
256 let mut output = Vec::new();
257 for (index, witness) in self.entries.iter().enumerate() {
258 if index != 0 {
259 output.push(b'\n');
260 }
261 let entry_start = output.len();
262 if serde_json::to_writer(&mut output, witness).is_err() {
263 output.truncate(entry_start);
264 }
265 }
266 String::from_utf8(output).unwrap_or_default()
267 }
268
269 pub fn clear(&mut self) {
271 self.entries.clear();
272 }
273}
274
275thread_local! {
277 static THREAD_LEDGER: RefCell<WitnessLedger> = RefCell::new(WitnessLedger::new());
278}
279
280#[must_use = "with_ledger returns None on reentrant calls; ignore explicitly with `let _ = with_ledger(...)`"]
293pub fn with_ledger<F, R>(f: F) -> Option<R>
294where
295 F: FnOnce(&mut WitnessLedger) -> R,
296{
297 THREAD_LEDGER.with(|cell| match cell.try_borrow_mut() {
298 Ok(mut guard) => Some(f(&mut guard)),
299 Err(_) => None,
300 })
301}
302
303pub fn drain_witnesses() -> Vec<ComplexityWitness> {
309 THREAD_LEDGER.with(|cell| match cell.try_borrow_mut() {
310 Ok(mut ledger) => std::mem::take(&mut ledger.entries),
311 Err(_) => Vec::new(),
312 })
313}
314
315thread_local! {
316 static WITNESS_COLLECTION_ACTIVE: Cell<bool> = const { Cell::new(false) };
317}
318
319#[must_use]
321pub fn witness_collection_enabled() -> bool {
322 WITNESS_COLLECTION_ACTIVE.with(Cell::get)
323}
324
325struct WitnessGuard {
326 was_active: bool,
327}
328
329impl Drop for WitnessGuard {
330 fn drop(&mut self) {
331 if !self.was_active {
332 WITNESS_COLLECTION_ACTIVE.with(|cell| cell.set(false));
333 }
334 }
335}
336
337pub fn collect_witnesses<F, R>(f: F) -> (R, Vec<ComplexityWitness>)
343where
344 F: FnOnce() -> R,
345{
346 let was_active = WITNESS_COLLECTION_ACTIVE.with(|cell| {
347 let active = cell.get();
348 if !active {
349 cell.set(true);
350 }
351 active
352 });
353
354 if !was_active {
355 let _ = with_ledger(WitnessLedger::clear);
358 }
359
360 let _guard = WitnessGuard { was_active };
361
362 let result = f();
363 let witnesses = if was_active {
364 Vec::new()
365 } else {
366 drain_witnesses()
367 };
368
369 (result, witnesses)
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct AlgorithmFamilyPolicy {
382 pub family: String,
383 pub algorithm: String,
384 pub policy: TieBreakPolicy,
385 pub dominant_complexity: String,
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
390#[serde(rename_all = "snake_case")]
391pub enum ReferenceAlgorithm {
392 Dijkstra,
393 BellmanFord,
394 Bfs,
395 Dfs,
396 MaxWeightMatching,
397 MinWeightMatching,
398 ConnectedComponents,
399 StronglyConnectedComponents,
400 Kruskal,
401 Prim,
402 EulerianCircuit,
403 TopologicalSort,
404}
405
406impl ReferenceAlgorithm {
407 pub const ALL: [Self; 12] = [
408 Self::Dijkstra,
409 Self::BellmanFord,
410 Self::Bfs,
411 Self::Dfs,
412 Self::MaxWeightMatching,
413 Self::MinWeightMatching,
414 Self::ConnectedComponents,
415 Self::StronglyConnectedComponents,
416 Self::Kruskal,
417 Self::Prim,
418 Self::EulerianCircuit,
419 Self::TopologicalSort,
420 ];
421
422 #[must_use]
423 pub const fn family(self) -> &'static str {
424 match self {
425 Self::Dijkstra | Self::BellmanFord => "shortest_path",
426 Self::Bfs | Self::Dfs => "traversal",
427 Self::MaxWeightMatching | Self::MinWeightMatching => "matching",
428 Self::ConnectedComponents | Self::StronglyConnectedComponents => "connectivity",
429 Self::Kruskal | Self::Prim => "trees",
430 Self::EulerianCircuit => "euler",
431 Self::TopologicalSort => "dag",
432 }
433 }
434
435 #[must_use]
436 pub const fn algorithm(self) -> &'static str {
437 match self {
438 Self::Dijkstra => "dijkstra",
439 Self::BellmanFord => "bellman_ford",
440 Self::Bfs => "bfs",
441 Self::Dfs => "dfs",
442 Self::MaxWeightMatching => "max_weight_matching",
443 Self::MinWeightMatching => "min_weight_matching",
444 Self::ConnectedComponents => "connected_components",
445 Self::StronglyConnectedComponents => "strongly_connected_components",
446 Self::Kruskal => "kruskal",
447 Self::Prim => "prim",
448 Self::EulerianCircuit => "eulerian_circuit",
449 Self::TopologicalSort => "topological_sort",
450 }
451 }
452
453 #[must_use]
454 pub const fn policy(self) -> TieBreakPolicy {
455 match self {
456 Self::Dijkstra => TieBreakPolicy::WeightThenInsertionOrder,
460 Self::BellmanFord => TieBreakPolicy::InsertionOrder,
461 Self::Bfs => TieBreakPolicy::InsertionOrder,
462 Self::Dfs => TieBreakPolicy::InsertionOrder,
463 Self::MaxWeightMatching => TieBreakPolicy::WeightThenLex,
464 Self::MinWeightMatching => TieBreakPolicy::WeightThenLex,
465 Self::ConnectedComponents => TieBreakPolicy::LexMin,
466 Self::StronglyConnectedComponents => TieBreakPolicy::InsertionOrder,
467 Self::Kruskal => TieBreakPolicy::WeightThenLex,
470 Self::Prim => TieBreakPolicy::WeightThenLex,
476 Self::EulerianCircuit => TieBreakPolicy::InsertionOrder,
477 Self::TopologicalSort => TieBreakPolicy::InsertionOrder,
478 }
479 }
480
481 #[must_use]
482 pub const fn dominant_complexity(self) -> &'static str {
483 match self {
484 Self::Dijkstra => "n_plus_m_log_n",
485 Self::BellmanFord => "n_m",
486 Self::Bfs => "n_plus_m",
487 Self::Dfs => "n_plus_m",
488 Self::MaxWeightMatching | Self::MinWeightMatching => "n_m_alpha",
489 Self::ConnectedComponents => "n_plus_m",
490 Self::StronglyConnectedComponents => "n_plus_m",
491 Self::Kruskal => "m_log_m",
492 Self::Prim => "m_log_n",
493 Self::EulerianCircuit => "m",
494 Self::TopologicalSort => "n_plus_m",
495 }
496 }
497
498 #[must_use]
499 pub fn policy_row(self) -> AlgorithmFamilyPolicy {
500 AlgorithmFamilyPolicy {
501 family: self.family().to_owned(),
502 algorithm: self.algorithm().to_owned(),
503 policy: self.policy(),
504 dominant_complexity: self.dominant_complexity().to_owned(),
505 }
506 }
507
508 #[must_use]
509 pub fn from_algorithm_id(algorithm: &str) -> Option<Self> {
510 Some(match algorithm {
511 "dijkstra" => Self::Dijkstra,
512 "bellman_ford" => Self::BellmanFord,
513 "bfs" => Self::Bfs,
514 "dfs" => Self::Dfs,
515 "max_weight_matching" => Self::MaxWeightMatching,
516 "min_weight_matching" => Self::MinWeightMatching,
517 "connected_components" => Self::ConnectedComponents,
518 "strongly_connected_components" => Self::StronglyConnectedComponents,
519 "kruskal" => Self::Kruskal,
520 "prim" => Self::Prim,
521 "eulerian_circuit" => Self::EulerianCircuit,
522 "topological_sort" => Self::TopologicalSort,
523 _ => return None,
524 })
525 }
526}
527
528#[must_use]
533pub fn v1_policy_registry() -> Vec<AlgorithmFamilyPolicy> {
534 ReferenceAlgorithm::ALL
535 .into_iter()
536 .map(ReferenceAlgorithm::policy_row)
537 .collect()
538}
539
540#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
546pub struct ComplexityBoundResult {
547 pub within_bounds: bool,
548 pub observed_count: u64,
549 pub upper_bound: u64,
550 pub dominant_term: String,
551 pub formula: String,
552}
553
554pub fn analytic_upper_bound(dominant_term: &str, n: usize, m: usize) -> Option<u64> {
556 let n = n as u64;
557 let m = m as u64;
558 match dominant_term {
559 "n" => Some(n),
560 "m" => Some(m),
561 "n_plus_m" => Some(n.saturating_add(m)),
562 "n_log_n" => {
563 let log_n = if n <= 1 {
564 1
565 } else {
566 (n as f64).log2().ceil() as u64
567 };
568 Some(n.saturating_mul(log_n))
569 }
570 "n_plus_m_log_n" => {
571 let log_n = if n <= 1 {
572 1
573 } else {
574 (n as f64).log2().ceil() as u64
575 };
576 Some(n.saturating_add(m.saturating_mul(log_n)))
577 }
578 "n_m" => Some(n.saturating_mul(m)),
579 "n_squared" => Some(n.saturating_mul(n)),
580 "n_m_alpha" => {
581 let alpha = if n <= 1 {
583 1
584 } else {
585 ((n as f64).log2().log2().ceil() as u64).max(1)
586 };
587 Some(n.saturating_mul(m).saturating_mul(alpha))
588 }
589 "m_log_m" => {
590 let log_m = if m <= 1 {
591 1
592 } else {
593 (m as f64).log2().ceil() as u64
594 };
595 Some(m.saturating_mul(log_m))
596 }
597 "m_log_n" => {
598 let log_n = if n <= 1 {
599 1
600 } else {
601 (n as f64).log2().ceil() as u64
602 };
603 Some(m.saturating_mul(log_n))
604 }
605 _ => None,
606 }
607}
608
609pub fn verify_complexity_bound(witness: &ComplexityWitness) -> Option<ComplexityBoundResult> {
612 let upper = analytic_upper_bound(&witness.dominant_term, witness.n, witness.m)?;
613 let adjusted_upper = upper.saturating_mul(2);
615 Some(ComplexityBoundResult {
616 within_bounds: witness.observed_count <= adjusted_upper,
617 observed_count: witness.observed_count,
618 upper_bound: adjusted_upper,
619 dominant_term: witness.dominant_term.clone(),
620 formula: format!("2 * f({}, {})", witness.n, witness.m),
621 })
622}
623
624pub fn assert_complexity_within_bounds(witness: &ComplexityWitness) {
626 if let Some(result) = verify_complexity_bound(witness) {
627 assert!(
628 result.within_bounds,
629 "Complexity bound violation: observed {} operations, expected at most {} for {} complexity (n={}, m={})",
630 result.observed_count, result.upper_bound, result.dominant_term, witness.n, witness.m
631 );
632 }
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize)]
644pub struct CounterExample {
645 pub algorithm: ReferenceAlgorithm,
647 pub graph_edges: Vec<(String, String)>,
649 pub node_count: usize,
651 pub directed: bool,
653 pub witness_a: ComplexityWitness,
655 pub witness_b: ComplexityWitness,
657 pub discrepancy: String,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
663pub struct MiningResult {
664 pub counter_examples: Vec<CounterExample>,
666 pub graphs_tested: u64,
668 pub executions: u64,
670 pub passing_algorithms: Vec<ReferenceAlgorithm>,
672}
673
674impl MiningResult {
675 #[must_use]
677 pub fn is_clean(&self) -> bool {
678 self.counter_examples.is_empty()
679 }
680}
681
682#[derive(Debug, Clone)]
684pub struct MiningConfig {
685 pub graphs_per_algorithm: usize,
687 pub executions_per_graph: usize,
689 pub max_nodes: usize,
691 pub max_density: f64,
693 pub seed: u64,
695}
696
697impl Default for MiningConfig {
698 fn default() -> Self {
699 Self {
700 graphs_per_algorithm: 100,
701 executions_per_graph: 3,
702 max_nodes: 20,
703 max_density: 0.5,
704 seed: 0,
705 }
706 }
707}
708
709#[must_use]
714pub fn compare_witnesses(a: &ComplexityWitness, b: &ComplexityWitness) -> Option<String> {
715 if a.decision_path_blake3 != b.decision_path_blake3 {
716 let hash_a = hex::encode(&a.decision_path_blake3[..8]);
717 let hash_b = hex::encode(&b.decision_path_blake3[..8]);
718 return Some(format!(
719 "Decision path hash mismatch: {}... vs {}... (counts: {} vs {})",
720 hash_a, hash_b, a.observed_count, b.observed_count
721 ));
722 }
723
724 if a.observed_count != b.observed_count {
725 return Some(format!(
726 "Operation count mismatch: {} vs {} (hashes match)",
727 a.observed_count, b.observed_count
728 ));
729 }
730
731 None
732}
733
734pub fn verify_witness_determinism<F>(
740 algorithm: ReferenceAlgorithm,
741 graph_edges: Vec<(String, String)>,
742 node_count: usize,
743 directed: bool,
744 iterations: usize,
745 mut run_algorithm: F,
746) -> Option<CounterExample>
747where
748 F: FnMut() -> Vec<ComplexityWitness>,
749{
750 if iterations < 2 {
751 return None;
752 }
753
754 let first_witnesses = run_algorithm();
755 let first_witness = first_witnesses.into_iter().next()?;
756
757 for _ in 1..iterations {
758 let witnesses = run_algorithm();
759 if let Some(witness) = witnesses.into_iter().next()
760 && let Some(discrepancy) = compare_witnesses(&first_witness, &witness)
761 {
762 return Some(CounterExample {
763 algorithm,
764 graph_edges,
765 node_count,
766 directed,
767 witness_a: first_witness,
768 witness_b: witness,
769 discrepancy,
770 });
771 }
772 }
773
774 None
775}
776
777#[must_use]
779fn seeded_random(seed: u64, index: u64) -> u64 {
780 let mut state = seed.wrapping_add(index);
782 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
783 state
784}
785
786#[must_use]
788pub fn generate_random_edges(
789 node_count: usize,
790 density: f64,
791 directed: bool,
792 seed: u64,
793) -> Vec<(String, String)> {
794 let mut edges = Vec::new();
795 let mut idx = 0u64;
796
797 for i in 0..node_count {
798 let j_start = if directed { 0 } else { i + 1 };
799 for j in j_start..node_count {
800 if i == j {
801 continue;
802 }
803 let rand = (seeded_random(seed, idx) % 1000) as f64 / 1000.0;
804 idx += 1;
805 if rand < density {
806 edges.push((i.to_string(), j.to_string()));
807 }
808 }
809 }
810
811 edges
812}
813
814#[must_use]
816pub fn mining_result_to_jsonl(result: &MiningResult) -> String {
817 let mut lines = Vec::new();
818
819 lines.push(
821 serde_json::json!({
822 "type": "mining_summary",
823 "graphs_tested": result.graphs_tested,
824 "executions": result.executions,
825 "counter_examples_found": result.counter_examples.len(),
826 "passing_algorithms": result.passing_algorithms.iter()
827 .map(|a| a.algorithm())
828 .collect::<Vec<_>>(),
829 })
830 .to_string(),
831 );
832
833 for ce in &result.counter_examples {
835 lines.push(serde_json::to_string(ce).unwrap_or_default());
836 }
837
838 lines.join("\n")
839}
840
841#[cfg(test)]
846mod tests {
847 use super::*;
848 use std::hint::black_box;
849 use std::time::Instant;
850
851 #[test]
852 fn test_policy_id_stable() {
853 assert_eq!(TieBreakPolicy::LexMin.id(), "lex_min");
854 assert_eq!(TieBreakPolicy::WeightThenLex.id(), "weight_then_lex");
855 assert_eq!(
856 TieBreakPolicy::DeterministicHash { seed: 42 }.id(),
857 "deterministic_hash"
858 );
859 }
860
861 #[test]
862 fn test_policy_roundtrip_serde() {
863 for policy in [
864 TieBreakPolicy::LexMin,
865 TieBreakPolicy::LexMax,
866 TieBreakPolicy::InsertionOrder,
867 TieBreakPolicy::ReverseInsertionOrder,
868 TieBreakPolicy::WeightThenLex,
869 TieBreakPolicy::LexThenWeight,
870 TieBreakPolicy::DeterministicHash { seed: 42 },
871 TieBreakPolicy::DegreeMinThenLex,
872 TieBreakPolicy::DegreeMaxThenLex,
873 TieBreakPolicy::DfsPreorder,
874 TieBreakPolicy::BfsLevelLex,
875 TieBreakPolicy::EdgeKeyLex,
876 ] {
877 let json = serde_json::to_string(&policy).unwrap();
878 let back: TieBreakPolicy = serde_json::from_str(&json).unwrap();
879 assert_eq!(policy, back);
880 }
881 }
882
883 #[test]
884 fn test_witness_sink_deterministic_hash() {
885 let mut sink1 = WitnessSink::new(TieBreakPolicy::LexMin);
886 sink1.record_decision("a", "b");
887 sink1.record_decision("c", "d");
888 let w1 = sink1.finalize(4, 3, "n_plus_m", None);
889
890 let mut sink2 = WitnessSink::new(TieBreakPolicy::LexMin);
891 sink2.record_decision("a", "b");
892 sink2.record_decision("c", "d");
893 let w2 = sink2.finalize(4, 3, "n_plus_m", None);
894
895 assert_eq!(w1.decision_path_blake3, w2.decision_path_blake3);
897 assert_eq!(w1.observed_count, 2);
898 }
899
900 #[test]
901 fn test_witness_sink_different_decisions_different_hash() {
902 let mut sink1 = WitnessSink::new(TieBreakPolicy::LexMin);
903 sink1.record_decision("a", "b");
904 let w1 = sink1.finalize(2, 1, "n", None);
905
906 let mut sink2 = WitnessSink::new(TieBreakPolicy::LexMin);
907 sink2.record_decision("b", "a"); let w2 = sink2.finalize(2, 1, "n", None);
909
910 assert_ne!(w1.decision_path_blake3, w2.decision_path_blake3);
912 }
913
914 #[test]
915 fn test_witness_ledger_jsonl() {
916 let mut ledger = WitnessLedger::new();
917 let mut sink = WitnessSink::new(TieBreakPolicy::InsertionOrder);
918 sink.record_decision("x", "y");
919 ledger.append(sink.finalize(3, 2, "n_plus_m", None));
920
921 let jsonl = ledger.to_jsonl();
922 assert!(!jsonl.is_empty());
923 let _: ComplexityWitness = serde_json::from_str(&jsonl).unwrap();
924 }
925
926 #[test]
927 #[ignore = "same-binary release-profile performance probe"]
928 fn witness_ledger_streaming_jsonl_ab() {
929 fn frozen_jsonl(ledger: &WitnessLedger) -> String {
930 ledger
931 .entries
932 .iter()
933 .map(|witness| serde_json::to_string(witness).unwrap_or_default())
934 .collect::<Vec<_>>()
935 .join("\n")
936 }
937
938 let policies = [
939 TieBreakPolicy::LexMin,
940 TieBreakPolicy::InsertionOrder,
941 TieBreakPolicy::WeightThenInsertionOrder,
942 TieBreakPolicy::DeterministicHash { seed: 0x5eed },
943 TieBreakPolicy::BfsLevelLex,
944 ];
945 let entry_count = 2_048usize;
946 let ledger = WitnessLedger {
947 entries: (0..entry_count)
948 .map(|index| ComplexityWitness {
949 n: index + 1,
950 m: index * 3 + 7,
951 dominant_term: match index % 3 {
952 0 => "n_plus_m",
953 1 => "n_log_n",
954 _ => "m_log_m",
955 }
956 .to_owned(),
957 observed_count: (index as u64 + 11) * 17,
958 policy: policies[index % policies.len()],
959 seed: (index % 4 == 0).then_some(index as u64),
960 decision_path_blake3: [(index * 29) as u8; 32],
961 })
962 .collect(),
963 };
964
965 assert_eq!(
966 WitnessLedger::new().to_jsonl(),
967 frozen_jsonl(&WitnessLedger::new())
968 );
969 assert_eq!(
970 ledger.to_jsonl(),
971 frozen_jsonl(&ledger),
972 "JSONL bytes drifted"
973 );
974
975 let rounds = 15usize;
976 let time = |streaming: bool| {
977 let started = Instant::now();
978 let output = if streaming {
979 ledger.to_jsonl()
980 } else {
981 frozen_jsonl(&ledger)
982 };
983 let elapsed = started.elapsed().as_nanos();
984 black_box(output);
985 elapsed
986 };
987 for _ in 0..3 {
988 black_box(time(false));
989 black_box(time(true));
990 }
991
992 let paired = |candidate: bool, baseline: bool| {
993 let mut baseline_ns = Vec::with_capacity(rounds);
994 let mut candidate_ns = Vec::with_capacity(rounds);
995 for round in 0..rounds {
996 let (base, cand) = if round % 2 == 0 {
997 (time(baseline), time(candidate))
998 } else {
999 let cand = time(candidate);
1000 let base = time(baseline);
1001 (base, cand)
1002 };
1003 baseline_ns.push(base);
1004 candidate_ns.push(cand);
1005 }
1006 (baseline_ns, candidate_ns)
1007 };
1008 let median = |samples: &[u128]| {
1009 let mut sorted = samples.to_vec();
1010 sorted.sort_unstable();
1011 sorted[sorted.len() / 2]
1012 };
1013 let report = |name: &str, baseline_ns: &[u128], candidate_ns: &[u128]| {
1014 let baseline_median = median(baseline_ns);
1015 let candidate_median = median(candidate_ns);
1016 let wins = baseline_ns
1017 .iter()
1018 .zip(candidate_ns)
1019 .filter(|(baseline, candidate)| baseline > candidate)
1020 .count();
1021 println!(
1022 "CGSE_WITNESS_JSONL_AB {name}: entries={entry_count} \
1023 baseline_median_ns={baseline_median} candidate_median_ns={candidate_median} \
1024 ratio={:.4}x wins={wins}/{rounds} exact_bytes=true",
1025 baseline_median as f64 / candidate_median as f64,
1026 );
1027 };
1028
1029 let (baseline_ns, candidate_ns) = paired(true, false);
1030 report("string_vec_join_vs_streaming", &baseline_ns, &candidate_ns);
1031 let (null_a_ns, null_b_ns) = paired(true, true);
1032 report("streaming_vs_streaming_null", &null_a_ns, &null_b_ns);
1033 }
1034
1035 #[test]
1036 fn test_thread_local_ledger() {
1037 let _ = with_ledger(|l| l.clear());
1038
1039 let mut sink = WitnessSink::new(TieBreakPolicy::BfsLevelLex);
1040 sink.record_decision("1", "2");
1041 let w = sink.finalize(5, 4, "n_plus_m", None);
1042 let _ = with_ledger(|l| l.append(w));
1043
1044 let drained = drain_witnesses();
1045 assert_eq!(drained.len(), 1);
1046 assert_eq!(drained[0].policy, TieBreakPolicy::BfsLevelLex);
1047 }
1048
1049 #[test]
1050 fn test_v1_registry_has_12_entries() {
1051 let reg = v1_policy_registry();
1052 assert_eq!(reg.len(), 12);
1053 }
1054
1055 #[test]
1056 fn test_v1_registry_unique_algorithms() {
1057 let reg = v1_policy_registry();
1058 let mut algos: Vec<&str> = reg.iter().map(|r| r.algorithm.as_str()).collect();
1059 algos.sort();
1060 algos.dedup();
1061 assert_eq!(algos.len(), 12, "All 12 algorithms should be unique");
1062 }
1063
1064 #[test]
1065 fn test_analytic_upper_bound_n_plus_m() {
1066 assert_eq!(analytic_upper_bound("n_plus_m", 100, 200), Some(300));
1067 }
1068
1069 #[test]
1070 fn test_analytic_upper_bound_n_log_n() {
1071 let bound = analytic_upper_bound("n_log_n", 1024, 0).unwrap();
1072 assert_eq!(bound, 10240);
1074 }
1075
1076 #[test]
1077 fn test_analytic_upper_bound_m_log_m() {
1078 let bound = analytic_upper_bound("m_log_m", 0, 256).unwrap();
1079 assert_eq!(bound, 2048);
1081 }
1082
1083 #[test]
1084 fn test_verify_complexity_bound_within() {
1085 let witness = ComplexityWitness {
1086 n: 100,
1087 m: 200,
1088 dominant_term: "n_plus_m".to_owned(),
1089 observed_count: 500, policy: TieBreakPolicy::LexMin,
1091 seed: None,
1092 decision_path_blake3: [0u8; 32],
1093 };
1094 let result = verify_complexity_bound(&witness).unwrap();
1095 assert!(result.within_bounds);
1096 assert_eq!(result.upper_bound, 600);
1097 }
1098
1099 #[test]
1100 fn test_verify_complexity_bound_exceeded() {
1101 let witness = ComplexityWitness {
1102 n: 100,
1103 m: 200,
1104 dominant_term: "n_plus_m".to_owned(),
1105 observed_count: 1000, policy: TieBreakPolicy::LexMin,
1107 seed: None,
1108 decision_path_blake3: [0u8; 32],
1109 };
1110 let result = verify_complexity_bound(&witness).unwrap();
1111 assert!(!result.within_bounds);
1112 }
1113
1114 #[test]
1115 fn test_analytic_upper_bound_unknown_term() {
1116 assert_eq!(analytic_upper_bound("unknown", 100, 200), None);
1117 }
1118
1119 #[test]
1120 fn test_collect_witnesses_panic_safety() {
1121 let result = std::panic::catch_unwind(|| {
1122 collect_witnesses(|| {
1123 std::panic::resume_unwind(Box::new("intentional panic"));
1124 });
1125 });
1126 assert!(result.is_err());
1127 assert!(!witness_collection_enabled());
1129 }
1130
1131 #[test]
1136 fn test_compare_witnesses_identical() {
1137 let w1 = ComplexityWitness {
1138 n: 10,
1139 m: 15,
1140 dominant_term: "n_plus_m".to_owned(),
1141 observed_count: 25,
1142 policy: TieBreakPolicy::LexMin,
1143 seed: None,
1144 decision_path_blake3: [42u8; 32],
1145 };
1146 let w2 = w1.clone();
1147 assert!(compare_witnesses(&w1, &w2).is_none());
1148 }
1149
1150 #[test]
1151 fn test_compare_witnesses_hash_mismatch() {
1152 let w1 = ComplexityWitness {
1153 n: 10,
1154 m: 15,
1155 dominant_term: "n_plus_m".to_owned(),
1156 observed_count: 25,
1157 policy: TieBreakPolicy::LexMin,
1158 seed: None,
1159 decision_path_blake3: [42u8; 32],
1160 };
1161 let w2 = ComplexityWitness {
1162 decision_path_blake3: [99u8; 32],
1163 ..w1.clone()
1164 };
1165 let result = compare_witnesses(&w1, &w2);
1166 assert!(result.is_some());
1167 assert!(result.unwrap().contains("hash mismatch"));
1168 }
1169
1170 #[test]
1171 fn test_compare_witnesses_count_mismatch() {
1172 let w1 = ComplexityWitness {
1173 n: 10,
1174 m: 15,
1175 dominant_term: "n_plus_m".to_owned(),
1176 observed_count: 25,
1177 policy: TieBreakPolicy::LexMin,
1178 seed: None,
1179 decision_path_blake3: [42u8; 32],
1180 };
1181 let w2 = ComplexityWitness {
1182 observed_count: 30,
1183 ..w1.clone()
1184 };
1185 let result = compare_witnesses(&w1, &w2);
1186 assert!(result.is_some());
1187 assert!(result.unwrap().contains("count mismatch"));
1188 }
1189
1190 #[test]
1191 fn test_generate_random_edges_deterministic() {
1192 let edges1 = generate_random_edges(5, 0.5, false, 12345);
1193 let edges2 = generate_random_edges(5, 0.5, false, 12345);
1194 assert_eq!(edges1, edges2);
1195 }
1196
1197 #[test]
1198 fn test_generate_random_edges_different_seeds() {
1199 let edges1 = generate_random_edges(5, 0.5, false, 12345);
1200 let edges2 = generate_random_edges(5, 0.5, false, 54321);
1201 assert_ne!(edges1, edges2);
1202 }
1203
1204 #[test]
1205 fn test_generate_random_edges_directed() {
1206 let edges = generate_random_edges(3, 1.0, true, 0);
1207 assert!(!edges.is_empty());
1210 }
1211
1212 #[test]
1213 fn test_mining_result_clean() {
1214 let result = MiningResult {
1215 counter_examples: vec![],
1216 graphs_tested: 100,
1217 executions: 300,
1218 passing_algorithms: ReferenceAlgorithm::ALL.to_vec(),
1219 };
1220 assert!(result.is_clean());
1221 }
1222
1223 #[test]
1224 fn test_mining_result_to_jsonl_format() {
1225 let result = MiningResult {
1226 counter_examples: vec![],
1227 graphs_tested: 10,
1228 executions: 30,
1229 passing_algorithms: vec![ReferenceAlgorithm::Bfs, ReferenceAlgorithm::Dfs],
1230 };
1231 let jsonl = mining_result_to_jsonl(&result);
1232 assert!(jsonl.contains("mining_summary"));
1233 assert!(jsonl.contains("graphs_tested"));
1234 assert!(jsonl.contains("10"));
1235 }
1236
1237 #[test]
1238 fn test_verify_witness_determinism_consistent() {
1239 let edges = vec![("0".to_string(), "1".to_string())];
1240 let mut call_count = 0;
1241
1242 let result =
1243 verify_witness_determinism(ReferenceAlgorithm::Bfs, edges, 2, false, 3, || {
1244 call_count += 1;
1245 vec![ComplexityWitness {
1246 n: 2,
1247 m: 1,
1248 dominant_term: "n_plus_m".to_owned(),
1249 observed_count: 3,
1250 policy: TieBreakPolicy::InsertionOrder,
1251 seed: None,
1252 decision_path_blake3: [1u8; 32],
1253 }]
1254 });
1255
1256 assert!(
1257 result.is_none(),
1258 "Consistent witnesses should not produce counter-example"
1259 );
1260 assert_eq!(call_count, 3);
1261 }
1262
1263 #[test]
1264 fn test_verify_witness_determinism_inconsistent() {
1265 let edges = vec![("0".to_string(), "1".to_string())];
1266 let mut call_count = 0;
1267
1268 let result =
1269 verify_witness_determinism(ReferenceAlgorithm::Bfs, edges, 2, false, 3, || {
1270 call_count += 1;
1271 vec![ComplexityWitness {
1272 n: 2,
1273 m: 1,
1274 dominant_term: "n_plus_m".to_owned(),
1275 observed_count: 3,
1276 policy: TieBreakPolicy::InsertionOrder,
1277 seed: None,
1278 decision_path_blake3: [call_count as u8; 32],
1280 }]
1281 });
1282
1283 assert!(
1284 result.is_some(),
1285 "Inconsistent witnesses should produce counter-example"
1286 );
1287 let ce = result.unwrap();
1288 assert_eq!(ce.algorithm, ReferenceAlgorithm::Bfs);
1289 assert!(ce.discrepancy.contains("hash mismatch"));
1290 }
1291
1292 #[test]
1293 fn test_mining_config_default() {
1294 let config = MiningConfig::default();
1295 assert_eq!(config.graphs_per_algorithm, 100);
1296 assert_eq!(config.executions_per_graph, 3);
1297 assert_eq!(config.max_nodes, 20);
1298 assert!(config.max_density > 0.0);
1299 }
1300}