1pub mod backend;
19pub mod encoder;
20pub mod fuzz;
21pub mod layer_inversion;
22pub mod obligations;
23pub mod proof;
24pub mod theory;
25pub mod unsat_core;
26
27#[cfg(feature = "smt-z3")]
28pub use backend::z3::Z3SmtBackendV0;
29pub use backend::{
30 SmtBackendCheckV0, SmtBackendKindV0, SmtBackendSatResultV0, SmtBackendV0, StubSmtBackendV0,
31};
32pub use encoder::{
33 CanonicalSmtInputV0, canonical_smt_input_v0, canonical_smt_input_with_script_v0,
34 canonical_smtlib2_script_v0,
35};
36pub use fuzz::{
37 SmtBisimulationFuzzCaseV0, SmtBisimulationFuzzReportV0, run_smt_bisimulation_fuzz_case_v0,
38 run_smt_bisimulation_fuzz_seed_corpus_v0, smt_bisimulation_fuzz_case_v0,
39};
40pub use layer_inversion::{
41 LayerFlattenInversionVerdictV0, LayerInversionDeclarationV0,
42 canonical_layer_flatten_inversion_input_v0, layer_inversion_declaration_v0,
43 smt_check_layer_flatten_inversion_v0,
44};
45pub use obligations::{
46 smt_evaluate_static_supports_condition_v0, smt_prove_box_shorthand_combination_v0,
47 smt_prove_layer_flatten_candidate_v0, smt_prove_scope_flatten_candidate_v0,
48};
49pub use proof::{
50 CascadeSMTProofAuditLogV0, CascadeSMTProofV0, SmtVerdictV0, cascade_spec_digest_v0,
51};
52pub use theory::{CascadeTheorySignatureV0, cascade_theory_signature_v0};
53pub use unsat_core::{CascadeUnsatCoreLabelV0, cascade_unsat_core_label_v0};
54
55pub const SMT_SCHEMA_VERSION_V0: &str = "0";
56pub const SMT_LAYER_MARKER_V0: &str = "smt-cascade-verification";
57pub const SMT_FEATURE_GATE_V0: &str = "smt-stub";
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use omena_cascade::{
63 BoxLonghandInputV0, LayerFlattenInputV0, ScopeFlattenInputV0, StaticSupportsAssumptionV0,
64 StaticSupportsEvalVerdictV0, evaluate_static_supports_condition,
65 prove_box_shorthand_combination, prove_layer_flatten_candidate,
66 prove_scope_flatten_candidate,
67 };
68
69 fn accepted_verdict(accepted: bool) -> SmtVerdictV0 {
70 if accepted {
71 SmtVerdictV0::Accepted
72 } else {
73 SmtVerdictV0::Rejected
74 }
75 }
76
77 #[test]
78 fn default_stub_backend_matches_l1_proof_verdict() {
79 let backend = StubSmtBackendV0::default();
80 let proof = smt_prove_box_shorthand_combination_v0(
81 "margin",
82 &[
83 BoxLonghandInputV0 {
84 property: "margin-top".to_string(),
85 value: "1px".to_string(),
86 important: false,
87 source_order: 1,
88 },
89 BoxLonghandInputV0 {
90 property: "margin-right".to_string(),
91 value: "1px".to_string(),
92 important: false,
93 source_order: 2,
94 },
95 BoxLonghandInputV0 {
96 property: "margin-bottom".to_string(),
97 value: "1px".to_string(),
98 important: false,
99 source_order: 3,
100 },
101 BoxLonghandInputV0 {
102 property: "margin-left".to_string(),
103 value: "1px".to_string(),
104 important: false,
105 source_order: 4,
106 },
107 ],
108 &backend,
109 );
110 assert_eq!(proof.schema_version, "0");
111 assert_eq!(proof.verdict, SmtVerdictV0::Accepted);
112 assert_eq!(proof.backend, SmtBackendKindV0::Stub);
113 assert!(
114 proof
115 .canonical_input
116 .smtlib2_script
117 .contains("(set-logic QF_UF)")
118 );
119 }
120
121 #[test]
122 fn cascade_spec_digest_is_hash_of_canonical_spec_material() {
123 let digest = cascade_spec_digest_v0();
124 assert_eq!(
125 digest,
126 *blake3::hash(crate::proof::CASCADE_SMT_SPEC_MATERIAL_V0.as_bytes()).as_bytes()
127 );
128 assert_ne!(digest, *b"omena-cascade-smt-spec-v0-------");
129 }
130
131 #[test]
132 fn proof_style_bisimulation_invariant_holds_for_all_l1_primitives() {
133 let backend = StubSmtBackendV0::default();
134 let longhands = vec![
135 BoxLonghandInputV0 {
136 property: "margin-top".to_string(),
137 value: "1px".to_string(),
138 important: false,
139 source_order: 1,
140 },
141 BoxLonghandInputV0 {
142 property: "margin-right".to_string(),
143 value: "1px".to_string(),
144 important: false,
145 source_order: 2,
146 },
147 BoxLonghandInputV0 {
148 property: "margin-bottom".to_string(),
149 value: "1px".to_string(),
150 important: false,
151 source_order: 3,
152 },
153 BoxLonghandInputV0 {
154 property: "margin-left".to_string(),
155 value: "1px".to_string(),
156 important: false,
157 source_order: 4,
158 },
159 ];
160 let l1_box = prove_box_shorthand_combination("margin", &longhands);
161 let l3_box = smt_prove_box_shorthand_combination_v0("margin", &longhands, &backend);
162 assert_eq!(l3_box.verdict, accepted_verdict(l1_box.accepted));
163
164 let scope_input = ScopeFlattenInputV0 {
165 root_selector: ":root".to_string(),
166 limit_selector: None,
167 scoped_rule_count: 1,
168 peer_scope_count: 0,
169 competing_unscoped_rule_count: 0,
170 inside_layer: false,
171 };
172 let l1_scope = prove_scope_flatten_candidate(scope_input.clone());
173 let l3_scope = smt_prove_scope_flatten_candidate_v0(scope_input, &backend);
174 assert_eq!(l3_scope.verdict, accepted_verdict(l1_scope.accepted));
175
176 let layer_input = LayerFlattenInputV0 {
177 layer_name: Some("components".to_string()),
178 layer_rule_count: 1,
179 peer_layer_count: 0,
180 unlayered_rule_count: 0,
181 important_declaration_count: 0,
182 closed_bundle: true,
183 };
184 let l1_layer = prove_layer_flatten_candidate(layer_input.clone());
185 let l3_layer = smt_prove_layer_flatten_candidate_v0(layer_input, &backend);
186 assert_eq!(l3_layer.verdict, accepted_verdict(l1_layer.accepted));
187 }
188
189 #[test]
190 fn static_supports_smt_equivalence_tracks_l1_verdict_shape() {
191 let backend = StubSmtBackendV0::default();
192 let l1 = evaluate_static_supports_condition(
193 "(display: grid)",
194 StaticSupportsAssumptionV0::ModernBrowser,
195 );
196 let l3 = smt_evaluate_static_supports_condition_v0(
197 "(display: grid)",
198 StaticSupportsAssumptionV0::ModernBrowser,
199 &backend,
200 );
201
202 assert_eq!(l1.verdict, StaticSupportsEvalVerdictV0::AlwaysTrue);
203 assert_eq!(l3.verdict, SmtVerdictV0::Accepted);
204 assert_eq!(l3.l1_primitive, "evaluate_static_supports_condition");
205 }
206
207 #[test]
208 fn smt_bisimulation_fuzz_seed_corpus_covers_m3_fixture_shapes() {
209 let report = run_smt_bisimulation_fuzz_seed_corpus_v0(128);
210 assert_eq!(report.schema_version, "0");
211 assert_eq!(report.fixture_suite, "m3-cascade-proof-fixtures");
212 assert_eq!(report.checked_obligation_count, 128 * 4);
213 assert_eq!(report.l1_l3_mismatch_count, 0);
214 assert!(report.passed);
215 }
216
217 #[test]
218 fn smt_bisimulation_fuzz_case_is_a_schema_zero_contract() {
219 let case = smt_bisimulation_fuzz_case_v0(42);
220 assert_eq!(case.schema_version, "0");
221 assert_eq!(case.layer_marker, "smt-cascade-verification");
222 assert_eq!(case.feature_gate, "smt-stub");
223 assert_eq!(case.seed, 42);
224 }
225
226 fn three_layer_declarations() -> [LayerInversionDeclarationV0; 3] {
227 [
228 layer_inversion_declaration_v0("base", 0, 0),
229 layer_inversion_declaration_v0("components", 1, 1),
230 layer_inversion_declaration_v0("utilities", 2, 2),
231 ]
232 }
233
234 #[test]
235 fn layer_inversion_encoder_emits_real_qf_lia_search() {
236 let input = canonical_layer_flatten_inversion_input_v0(&three_layer_declarations());
239 assert!(input.smtlib2_script.contains("(set-logic QF_LIA)"));
240 assert!(input.smtlib2_script.contains("(declare-const rank_0 Int)"));
241 assert!(
242 input
243 .smtlib2_script
244 .contains(":named cascade_layer_flatten_inversion")
245 );
246 assert!(input.smtlib2_script.contains("(> rank_"));
247 assert!(input.smtlib2_script.contains("(> source_"));
248 assert!(
251 input
252 .canonical_terms
253 .iter()
254 .all(|term| !term.starts_with("require:"))
255 );
256 }
257
258 #[test]
259 fn stub_backend_cannot_reason_about_layer_inversion() {
260 let backend = StubSmtBackendV0::default();
265
266 let mut inverted = three_layer_declarations();
267 inverted[2].source_order = -1;
269 let stub_inverted = smt_check_layer_flatten_inversion_v0(&inverted, &backend);
270
271 let safe = three_layer_declarations();
272 let stub_safe = smt_check_layer_flatten_inversion_v0(&safe, &backend);
273
274 assert_eq!(stub_inverted.backend, SmtBackendKindV0::Stub);
275 assert_eq!(stub_inverted.sat_result, SmtBackendSatResultV0::Sat);
276 assert_eq!(stub_safe.sat_result, SmtBackendSatResultV0::Sat);
277 assert_eq!(stub_inverted.verdict, stub_safe.verdict);
279 }
280
281 #[cfg(feature = "smt-z3")]
282 #[test]
283 fn z3_decides_layer_inversion_and_disagrees_with_stub() {
284 let z3 = Z3SmtBackendV0::default();
285 let stub = StubSmtBackendV0::default();
286
287 let mut inverted = three_layer_declarations();
291 inverted[2].source_order = -1;
292 let z3_inverted = smt_check_layer_flatten_inversion_v0(&inverted, &z3);
293
294 let safe = three_layer_declarations();
297 let z3_safe = smt_check_layer_flatten_inversion_v0(&safe, &z3);
298
299 assert_eq!(z3_inverted.backend, SmtBackendKindV0::Z3);
302 assert_eq!(z3_inverted.sat_result, SmtBackendSatResultV0::Sat);
303 assert!(z3_inverted.inversion_exists);
304 assert_eq!(z3_inverted.verdict, SmtVerdictV0::Rejected);
305
306 assert_eq!(z3_safe.sat_result, SmtBackendSatResultV0::Unsat);
307 assert!(!z3_safe.inversion_exists);
308 assert_eq!(z3_safe.verdict, SmtVerdictV0::Accepted);
309
310 let stub_safe = smt_check_layer_flatten_inversion_v0(&safe, &stub);
314 assert_eq!(stub_safe.sat_result, SmtBackendSatResultV0::Sat);
315 assert_ne!(z3_safe.sat_result, stub_safe.sat_result);
316 assert_ne!(z3_safe.verdict, stub_safe.verdict);
317 }
318
319 #[cfg(feature = "smt-z3")]
320 #[test]
321 fn z3_backend_solves_canonical_box_shorthand_obligation() {
322 let backend = Z3SmtBackendV0::default();
323 let accepted = smt_prove_box_shorthand_combination_v0(
324 "margin",
325 &[
326 BoxLonghandInputV0 {
327 property: "margin-top".to_string(),
328 value: "1px".to_string(),
329 important: false,
330 source_order: 1,
331 },
332 BoxLonghandInputV0 {
333 property: "margin-right".to_string(),
334 value: "1px".to_string(),
335 important: false,
336 source_order: 2,
337 },
338 BoxLonghandInputV0 {
339 property: "margin-bottom".to_string(),
340 value: "1px".to_string(),
341 important: false,
342 source_order: 3,
343 },
344 BoxLonghandInputV0 {
345 property: "margin-left".to_string(),
346 value: "1px".to_string(),
347 important: false,
348 source_order: 4,
349 },
350 ],
351 &backend,
352 );
353 let rejected = smt_prove_box_shorthand_combination_v0(
354 "margin",
355 &[
356 BoxLonghandInputV0 {
357 property: "margin-top".to_string(),
358 value: "1px".to_string(),
359 important: false,
360 source_order: 1,
361 },
362 BoxLonghandInputV0 {
363 property: "margin-bottom".to_string(),
364 value: "1px".to_string(),
365 important: false,
366 source_order: 2,
367 },
368 BoxLonghandInputV0 {
369 property: "margin-right".to_string(),
370 value: "1px".to_string(),
371 important: false,
372 source_order: 3,
373 },
374 BoxLonghandInputV0 {
375 property: "margin-left".to_string(),
376 value: "1px".to_string(),
377 important: false,
378 source_order: 4,
379 },
380 ],
381 &backend,
382 );
383
384 assert_eq!(accepted.backend, SmtBackendKindV0::Z3);
385 assert_eq!(accepted.solver_check.sat_result, SmtBackendSatResultV0::Sat);
386 assert_eq!(accepted.verdict, SmtVerdictV0::Accepted);
387 assert!(
388 accepted
389 .canonical_input
390 .smtlib2_script
391 .contains("(assert (! true :named req_canonical_longhand_quartet))")
392 );
393 assert_eq!(
394 rejected.solver_check.sat_result,
395 SmtBackendSatResultV0::Unsat
396 );
397 assert_eq!(rejected.verdict, SmtVerdictV0::Rejected);
398 assert!(
399 rejected
400 .canonical_input
401 .smtlib2_script
402 .contains("(assert (! false :named req_canonical_longhand_quartet))")
403 );
404 assert_ne!(
405 accepted.canonical_input.canonical_terms,
406 rejected.canonical_input.canonical_terms
407 );
408 }
409}