1use omena_cascade::{
8 BoxLonghandInputV0, LayerFlattenInputV0, LonghandMergeInputV0, ScopeFlattenInputV0,
9 StaticSupportsAssumptionV0, StaticSupportsEvalVerdictV0, evaluate_static_supports_condition,
10 prove_box_shorthand_combination, prove_layer_flatten_candidate, prove_longhand_merge,
11 prove_scope_flatten_candidate,
12};
13use omena_evidence_graph::{
14 EvidenceDemandEdgeV0, EvidenceGraphBuildErrorV0, EvidenceGraphV0, EvidenceNodeKeyV0,
15 EvidenceNodeSeedV0, FamilyStampV0, GuaranteeKindV0, LedgerDischargeWitnessV0,
16 ObligationFamilyIdV0, ProseObligationProvenanceV0, build_evidence_graph_from_edges_v0,
17};
18use omena_refinement_trait::RefinementVerdictV0;
19use serde::Serialize;
20
21pub mod discharge_ledger;
22pub mod fuzz;
23pub mod proof_kernel;
24
25pub use discharge_ledger::{
26 DISCHARGE_LEDGER_PRODUCT_V1, DISCHARGE_LEDGER_SCHEMA_VERSION_V1, DischargeLedgerLookupStatusV0,
27 DischargeLedgerLookupV0, DischargeLedgerVerdictV0, discharge_ledger_cell_key_v0,
28 lookup_discharge_ledger_entry_v0,
29};
30pub use fuzz::{
31 SmtBisimulationFuzzCaseV0, SmtBisimulationFuzzReportV0, run_smt_bisimulation_fuzz_case_v0,
32 run_smt_bisimulation_fuzz_seed_corpus_v0, smt_bisimulation_fuzz_case_v0,
33};
34pub use proof_kernel::*;
35
36pub const SMT_SCHEMA_VERSION_V0: &str = "0";
37pub const SMT_LAYER_MARKER_V0: &str = "smt-cascade-verification";
38pub const SMT_FEATURE_GATE_V0: &str = "smt-stub";
39const REWRITE_PROOF_INPUT_EVIDENCE_QUERY_V0: &str = "omena-cascade-proof.transform-rewrite-input";
40const CASCADE_PROOF_RECORD_EVIDENCE_QUERY_V0: &str = "omena-cascade-proof.cascade-proof-record";
41const CASCADE_PROOF_EVIDENCE_EDGE_KIND_V0: &str = "cascade-proof-evidence";
42pub const TRANSFORM_REWRITE_PROOF_INPUT_OBLIGATION_FAMILY_V0: ObligationFamilyIdV0 =
43 ObligationFamilyIdV0::CascadeObligationDeclaration;
44
45fn prose_obligation_family_stamp(provenance: &[String]) -> FamilyStampV0 {
46 let Some(prose_provenance) = ProseObligationProvenanceV0::from_provenance_labels(provenance)
47 else {
48 unreachable!("prose evidence seeds include an obligation provenance label")
49 };
50 FamilyStampV0::prose_obligation_discharged(&prose_provenance)
51}
52
53const CASCADE_SMT_SPEC_MATERIAL_V0: &str = "\
54schema=0\n\
55theory=cascade-smt-theory-v0\n\
56encoding=canonical-smt-input-v0\n\
57default-backend=stub-propositional\n\
58opt-in-backend=smt-z3-qf-lia-layer-inversion\n\
59obligations=box-shorthand-combination,scope-flatten-candidate,layer-flatten-candidate,static-supports-condition\n\
60";
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct CanonicalSmtInputV0 {
65 pub schema_version: &'static str,
66 pub product: &'static str,
67 pub layer_marker: &'static str,
68 pub feature_gate: &'static str,
69 pub obligation_id: String,
70 pub l1_primitive: &'static str,
71 pub canonical_terms: Vec<String>,
72 pub smtlib2_script: String,
73}
74
75pub fn canonical_smt_input_v0(
76 obligation_id: impl Into<String>,
77 l1_primitive: &'static str,
78 canonical_terms: Vec<String>,
79) -> CanonicalSmtInputV0 {
80 let smtlib2_script = canonical_smtlib2_script_v0(&canonical_terms);
81 CanonicalSmtInputV0 {
82 schema_version: SMT_SCHEMA_VERSION_V0,
83 product: "omena-smt.canonical-input",
84 layer_marker: SMT_LAYER_MARKER_V0,
85 feature_gate: SMT_FEATURE_GATE_V0,
86 obligation_id: obligation_id.into(),
87 l1_primitive,
88 canonical_terms,
89 smtlib2_script,
90 }
91}
92
93pub fn canonical_smt_input_with_script_v0(
94 obligation_id: impl Into<String>,
95 l1_primitive: &'static str,
96 canonical_terms: Vec<String>,
97 smtlib2_script: String,
98) -> CanonicalSmtInputV0 {
99 CanonicalSmtInputV0 {
100 schema_version: SMT_SCHEMA_VERSION_V0,
101 product: "omena-smt.canonical-input",
102 layer_marker: SMT_LAYER_MARKER_V0,
103 feature_gate: SMT_FEATURE_GATE_V0,
104 obligation_id: obligation_id.into(),
105 l1_primitive,
106 canonical_terms,
107 smtlib2_script,
108 }
109}
110
111pub fn canonical_smtlib2_script_v0(canonical_terms: &[String]) -> String {
112 let mut script = String::from("(set-logic QF_UF)\n");
113 for term in canonical_terms {
114 if let Some((name, value)) = canonical_requirement_parts_v0(term) {
115 let symbol = smtlib2_named_assertion_symbol_v0(name);
116 let atom = if value { "true" } else { "false" };
117 script.push_str(&format!("(assert (! {atom} :named {symbol}))\n"));
118 } else {
119 let comment = smtlib2_comment_v0(term);
120 script.push_str(&format!("; {comment}\n"));
121 }
122 }
123 script
124}
125
126pub fn canonical_requirement_value_v0(term: &str) -> Option<bool> {
127 canonical_requirement_parts_v0(term).map(|(_, value)| value)
128}
129
130pub fn canonical_input_has_unknown_v0(input: &CanonicalSmtInputV0) -> bool {
131 input
132 .canonical_terms
133 .iter()
134 .any(|term| term.starts_with("unknown:"))
135}
136
137fn canonical_requirement_parts_v0(term: &str) -> Option<(&str, bool)> {
138 let (name, value) = term.strip_prefix("require:")?.rsplit_once('=')?;
139 match value {
140 "true" => Some((name, true)),
141 "false" => Some((name, false)),
142 _ => None,
143 }
144}
145
146fn smtlib2_named_assertion_symbol_v0(name: &str) -> String {
147 let mut symbol = String::from("req_");
148 for ch in name.chars() {
149 if ch.is_ascii_alphanumeric() {
150 symbol.push(ch);
151 } else {
152 symbol.push('_');
153 }
154 }
155 symbol
156}
157
158fn smtlib2_comment_v0(term: &str) -> String {
159 term.chars()
160 .map(|ch| match ch {
161 '\n' | '\r' => ' ',
162 _ => ch,
163 })
164 .collect()
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
168#[serde(rename_all = "camelCase")]
169pub enum SmtBackendKindV0 {
170 Stub,
171 Z3,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
175#[serde(rename_all = "camelCase")]
176pub enum SmtBackendSatResultV0 {
177 Sat,
178 Unsat,
179 Unknown,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
183#[serde(rename_all = "camelCase")]
184pub struct SmtBackendCheckV0 {
185 pub schema_version: &'static str,
186 pub product: &'static str,
187 pub layer_marker: &'static str,
188 pub feature_gate: &'static str,
189 pub backend: SmtBackendKindV0,
190 pub obligation_id: String,
191 pub formula_count: usize,
192 pub sat_result: SmtBackendSatResultV0,
193 pub model_available: bool,
194}
195
196pub trait SmtBackendV0 {
197 fn backend_kind(&self) -> SmtBackendKindV0;
198
199 fn quantifier_elimination_tactic(&self) -> Option<&'static str> {
200 None
201 }
202
203 fn check_canonical_input_v0(&self, input: &CanonicalSmtInputV0) -> SmtBackendCheckV0 {
204 let sat_result = if canonical_input_has_unknown_v0(input) {
205 SmtBackendSatResultV0::Unknown
206 } else if input
207 .canonical_terms
208 .iter()
209 .all(|term| canonical_requirement_value_v0(term).unwrap_or(true))
214 {
215 SmtBackendSatResultV0::Sat
216 } else {
217 SmtBackendSatResultV0::Unsat
218 };
219 SmtBackendCheckV0 {
220 schema_version: SMT_SCHEMA_VERSION_V0,
221 product: "omena-smt.backend-check",
222 layer_marker: SMT_LAYER_MARKER_V0,
223 feature_gate: SMT_FEATURE_GATE_V0,
224 backend: self.backend_kind(),
225 obligation_id: input.obligation_id.clone(),
226 formula_count: input.canonical_terms.len(),
227 sat_result,
228 model_available: matches!(sat_result, SmtBackendSatResultV0::Sat),
229 }
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
234#[serde(rename_all = "camelCase")]
235pub struct StubSmtBackendV0 {
236 pub schema_version: &'static str,
237 pub product: &'static str,
238 pub layer_marker: &'static str,
239 pub feature_gate: &'static str,
240}
241
242impl Default for StubSmtBackendV0 {
243 fn default() -> Self {
244 Self {
245 schema_version: SMT_SCHEMA_VERSION_V0,
246 product: "omena-smt.backend.stub",
247 layer_marker: SMT_LAYER_MARKER_V0,
248 feature_gate: SMT_FEATURE_GATE_V0,
249 }
250 }
251}
252
253impl SmtBackendV0 for StubSmtBackendV0 {
254 fn backend_kind(&self) -> SmtBackendKindV0 {
255 SmtBackendKindV0::Stub
256 }
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
260#[serde(rename_all = "camelCase")]
261pub enum SmtVerdictV0 {
262 Accepted,
263 Rejected,
264 Unknown,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
268#[serde(rename_all = "camelCase")]
269pub struct CascadeSMTProofV0 {
270 pub schema_version: &'static str,
271 pub product: &'static str,
272 pub layer_marker: &'static str,
273 pub feature_gate: &'static str,
274 pub obligation_id: String,
275 pub backend: SmtBackendKindV0,
276 pub verdict: SmtVerdictV0,
277 pub l1_primitive: &'static str,
278 pub l1_accepted: Option<bool>,
279 pub canonical_input: CanonicalSmtInputV0,
280 pub solver_check: SmtBackendCheckV0,
281 pub refinement_verdict: Option<RefinementVerdictV0>,
282 pub cascade_spec_digest: [u8; 32],
283}
284
285impl CascadeSMTProofV0 {
286 pub fn evidence_node_key(&self) -> EvidenceNodeKeyV0 {
287 EvidenceNodeKeyV0::new(
288 CASCADE_PROOF_RECORD_EVIDENCE_QUERY_V0,
289 self.obligation_id.clone(),
290 )
291 }
292
293 pub fn evidence_node_seed(&self) -> EvidenceNodeSeedV0 {
294 let provenance = vec![
295 ["obligation:", self.obligation_id.as_str()].concat(),
296 ["primitive:", self.l1_primitive].concat(),
297 ["featureGate:", self.feature_gate].concat(),
298 ];
299 let lookup = lookup_discharge_ledger_entry_v0(&self.canonical_input);
300 if let Some(seed) = ledger_backed_cascade_proof_seed_v0(
301 self.evidence_node_key(),
302 provenance.clone(),
303 &lookup,
304 ) {
305 return seed;
306 }
307 let family_stamp = prose_obligation_family_stamp(&provenance);
308 EvidenceNodeSeedV0::with_family(
309 self.evidence_node_key(),
310 provenance,
311 GuaranteeKindV0::for_label_less_family(),
312 family_stamp,
313 )
314 }
315
316 pub fn evidence_graph(&self) -> Result<EvidenceGraphV0, EvidenceGraphBuildErrorV0> {
317 build_evidence_graph_from_edges_v0(
318 [self.evidence_node_seed()],
319 [EvidenceDemandEdgeV0::new(
320 CASCADE_PROOF_RECORD_EVIDENCE_QUERY_V0,
321 self.evidence_node_key(),
322 CASCADE_PROOF_EVIDENCE_EDGE_KIND_V0,
323 )],
324 )
325 }
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
329#[serde(rename_all = "camelCase")]
330pub struct TransformRewriteProofInputV0 {
331 pub schema_version: &'static str,
332 pub product: &'static str,
333 pub pass_id: String,
334 pub cascade_obligation_declared: bool,
335 pub provenance_recomputed: bool,
336 pub provenance_preserved: bool,
337 pub contains_bogus_or_trivia: bool,
338 pub stable_post_semantic_ir: bool,
339}
340
341impl TransformRewriteProofInputV0 {
342 pub fn new(
343 pass_id: impl Into<String>,
344 obligation_family: ObligationFamilyIdV0,
345 provenance_recomputed: bool,
346 provenance_preserved: bool,
347 contains_bogus_or_trivia: bool,
348 stable_post_semantic_ir: bool,
349 ) -> Self {
350 let cascade_obligation_declared = obligation_family.declares_cascade_obligation();
351 Self {
352 schema_version: SMT_SCHEMA_VERSION_V0,
353 product: "omena-cascade-proof.transform-rewrite-input",
354 pass_id: pass_id.into(),
355 cascade_obligation_declared,
356 provenance_recomputed,
357 provenance_preserved,
358 contains_bogus_or_trivia,
359 stable_post_semantic_ir,
360 }
361 }
362
363 pub fn evidence_node_key(&self) -> EvidenceNodeKeyV0 {
364 EvidenceNodeKeyV0::new(REWRITE_PROOF_INPUT_EVIDENCE_QUERY_V0, self.pass_id.clone())
365 }
366
367 pub fn evidence_node_seed(&self) -> EvidenceNodeSeedV0 {
368 let provenance = vec![
369 ["pass:", self.pass_id.as_str()].concat(),
370 [
371 "cascadeObligationDeclared:",
372 self.cascade_obligation_declared.to_string().as_str(),
373 ]
374 .concat(),
375 [
376 "provenanceRecomputed:",
377 self.provenance_recomputed.to_string().as_str(),
378 ]
379 .concat(),
380 [
381 "provenancePreserved:",
382 self.provenance_preserved.to_string().as_str(),
383 ]
384 .concat(),
385 ];
386 let family_stamp = prose_obligation_family_stamp(&provenance);
387 EvidenceNodeSeedV0::with_family(
388 self.evidence_node_key(),
389 provenance,
390 GuaranteeKindV0::for_label_less_family(),
391 family_stamp,
392 )
393 }
394
395 pub fn evidence_graph(&self) -> Result<EvidenceGraphV0, EvidenceGraphBuildErrorV0> {
396 build_evidence_graph_from_edges_v0(
397 [self.evidence_node_seed()],
398 [EvidenceDemandEdgeV0::new(
399 REWRITE_PROOF_INPUT_EVIDENCE_QUERY_V0,
400 self.evidence_node_key(),
401 CASCADE_PROOF_EVIDENCE_EDGE_KIND_V0,
402 )],
403 )
404 }
405}
406
407fn ledger_backed_cascade_proof_seed_v0(
408 key: EvidenceNodeKeyV0,
409 mut provenance: Vec<String>,
410 lookup: &DischargeLedgerLookupV0,
411) -> Option<EvidenceNodeSeedV0> {
412 if !lookup.can_apply_family_stamp() {
413 return None;
414 }
415 let witness = LedgerDischargeWitnessV0::from_discharge_cell_key_v0(&lookup.cell_key)?;
416 provenance.push(["dischargeCell:", lookup.cell_key.as_str()].concat());
417 Some(EvidenceNodeSeedV0::with_family(
418 key,
419 provenance,
420 GuaranteeKindV0::for_label_less_family(),
421 FamilyStampV0::ledger_backed_obligation_discharge(&witness),
422 ))
423}
424
425pub fn cascade_spec_digest_v0() -> [u8; 32] {
426 *blake3::hash(CASCADE_SMT_SPEC_MATERIAL_V0.as_bytes()).as_bytes()
427}
428
429fn cascade_smt_proof_v0<B: SmtBackendV0>(
430 canonical_input: CanonicalSmtInputV0,
431 backend: &B,
432 l1_primitive: &'static str,
433 l1_accepted: Option<bool>,
434) -> CascadeSMTProofV0 {
435 let solver_check = backend.check_canonical_input_v0(&canonical_input);
436 CascadeSMTProofV0 {
437 schema_version: SMT_SCHEMA_VERSION_V0,
438 product: "omena-smt.cascade-proof",
439 layer_marker: SMT_LAYER_MARKER_V0,
440 feature_gate: SMT_FEATURE_GATE_V0,
441 obligation_id: canonical_input.obligation_id.clone(),
442 backend: backend.backend_kind(),
443 verdict: smt_verdict_from_backend_check_v0(solver_check.sat_result),
444 l1_primitive,
445 l1_accepted,
446 canonical_input,
447 solver_check,
448 refinement_verdict: None,
449 cascade_spec_digest: cascade_spec_digest_v0(),
450 }
451}
452
453fn smt_verdict_from_backend_check_v0(sat_result: SmtBackendSatResultV0) -> SmtVerdictV0 {
454 match sat_result {
455 SmtBackendSatResultV0::Sat => SmtVerdictV0::Accepted,
456 SmtBackendSatResultV0::Unsat => SmtVerdictV0::Rejected,
457 SmtBackendSatResultV0::Unknown => SmtVerdictV0::Unknown,
458 }
459}
460
461pub fn smt_prove_box_shorthand_combination_v0<B: SmtBackendV0>(
462 shorthand_property: &str,
463 longhands: &[BoxLonghandInputV0],
464 backend: &B,
465) -> CascadeSMTProofV0 {
466 let proof = prove_box_shorthand_combination(shorthand_property, longhands);
467 let canonical_input =
468 canonical_box_shorthand_combination_input_v0(shorthand_property, longhands);
469 cascade_smt_proof_v0(
470 canonical_input,
471 backend,
472 "prove_box_shorthand_combination",
473 Some(proof.accepted),
474 )
475}
476
477pub fn smt_prove_longhand_merge_v0<B, S>(
478 shorthand_property: &str,
479 expected_longhands: &[S],
480 longhands: &[LonghandMergeInputV0],
481 backend: &B,
482) -> CascadeSMTProofV0
483where
484 B: SmtBackendV0,
485 S: AsRef<str>,
486{
487 let proof = prove_longhand_merge(shorthand_property, expected_longhands, longhands);
488 let canonical_input =
489 canonical_longhand_merge_input_v0(shorthand_property, expected_longhands, longhands);
490 cascade_smt_proof_v0(
491 canonical_input,
492 backend,
493 "prove_longhand_merge",
494 Some(proof.accepted),
495 )
496}
497
498pub fn smt_prove_scope_flatten_candidate_v0<B: SmtBackendV0>(
499 input: ScopeFlattenInputV0,
500 backend: &B,
501) -> CascadeSMTProofV0 {
502 let canonical_input = canonical_scope_flatten_candidate_input_v0(&input);
503 let proof = prove_scope_flatten_candidate(input);
504 cascade_smt_proof_v0(
505 canonical_input,
506 backend,
507 "prove_scope_flatten_candidate",
508 Some(proof.accepted),
509 )
510}
511
512pub fn smt_prove_layer_flatten_candidate_v0<B: SmtBackendV0>(
513 input: LayerFlattenInputV0,
514 backend: &B,
515) -> CascadeSMTProofV0 {
516 let canonical_input = canonical_layer_flatten_candidate_input_v0(&input);
517 let proof = prove_layer_flatten_candidate(input);
518 cascade_smt_proof_v0(
519 canonical_input,
520 backend,
521 "prove_layer_flatten_candidate",
522 Some(proof.accepted),
523 )
524}
525
526pub fn smt_evaluate_static_supports_condition_v0<B: SmtBackendV0>(
527 condition: &str,
528 assumption: StaticSupportsAssumptionV0,
529 backend: &B,
530) -> CascadeSMTProofV0 {
531 let witness = evaluate_static_supports_condition(condition, assumption);
532 let l1_accepted = match witness.verdict {
533 StaticSupportsEvalVerdictV0::AlwaysTrue => Some(true),
534 StaticSupportsEvalVerdictV0::AlwaysFalse => Some(false),
535 StaticSupportsEvalVerdictV0::Unknown => None,
536 };
537 cascade_smt_proof_v0(
538 canonical_static_supports_condition_input_v0(&witness.verdict),
539 backend,
540 "evaluate_static_supports_condition",
541 l1_accepted,
542 )
543}
544
545pub fn smt_verify_transform_rewrite_candidate_v0<B: SmtBackendV0>(
546 input: &TransformRewriteProofInputV0,
547 backend: &B,
548) -> CascadeSMTProofV0 {
549 cascade_smt_proof_v0(
550 canonical_transform_rewrite_candidate_input_v0(input),
551 backend,
552 "verify_transform_rewrite_candidate",
553 Some(
554 input.cascade_obligation_declared
555 && input.provenance_recomputed
556 && input.provenance_preserved
557 && !input.contains_bogus_or_trivia
558 && input.stable_post_semantic_ir,
559 ),
560 )
561}
562
563fn canonical_box_shorthand_combination_input_v0(
564 shorthand_property: &str,
565 longhands: &[BoxLonghandInputV0],
566) -> CanonicalSmtInputV0 {
567 let expected = smt_box_shorthand_longhands_v0(shorthand_property);
568 let canonical_order = expected.is_some_and(|expected| {
569 longhands.len() == expected.len()
570 && longhands
571 .iter()
572 .zip(expected.iter())
573 .all(|(actual, expected)| actual.property == *expected)
574 });
575 canonical_smt_input_v0(
576 "box-shorthand-combination",
577 "prove_box_shorthand_combination",
578 vec![
579 smt_ir_computed_requirement_v0("supported-shorthand-property", expected.is_some()),
580 smt_ir_computed_requirement_v0("canonical-longhand-quartet", canonical_order),
581 smt_ir_computed_requirement_v0(
582 "no-important-longhand",
583 longhands.iter().all(|longhand| !longhand.important),
584 ),
585 smt_ir_computed_requirement_v0(
586 "no-empty-longhand-value",
587 longhands.iter().all(|longhand| !longhand.value.is_empty()),
588 ),
589 smt_ir_computed_requirement_v0(
590 "adjacent-source-order",
591 longhands
592 .windows(2)
593 .all(|pair| pair[1].source_order == pair[0].source_order + 1),
594 ),
595 ],
596 )
597}
598
599fn canonical_longhand_merge_input_v0<S>(
600 shorthand_property: &str,
601 expected_longhands: &[S],
602 longhands: &[LonghandMergeInputV0],
603) -> CanonicalSmtInputV0
604where
605 S: AsRef<str>,
606{
607 let canonical_order = !expected_longhands.is_empty()
608 && longhands.len() == expected_longhands.len()
609 && longhands
610 .iter()
611 .zip(expected_longhands.iter())
612 .all(|(actual, expected)| actual.property == expected.as_ref());
613 canonical_smt_input_v0(
614 "longhand-merge",
615 "prove_longhand_merge",
616 vec![
617 smt_ir_computed_requirement_v0(
618 "supported-merge-family",
619 !expected_longhands.is_empty(),
620 ),
621 smt_ir_computed_requirement_v0("canonical-longhand-order", canonical_order),
622 smt_ir_computed_requirement_v0(
623 "no-important-longhand",
624 longhands.iter().all(|longhand| !longhand.important),
625 ),
626 smt_ir_computed_requirement_v0(
627 "no-empty-longhand-value",
628 longhands.iter().all(|longhand| !longhand.value.is_empty()),
629 ),
630 smt_ir_computed_requirement_v0(
631 "adjacent-source-order",
632 longhands
633 .windows(2)
634 .all(|pair| pair[1].source_order == pair[0].source_order + 1),
635 ),
636 format!("merge-family:{shorthand_property}"),
637 ],
638 )
639}
640
641fn canonical_scope_flatten_candidate_input_v0(input: &ScopeFlattenInputV0) -> CanonicalSmtInputV0 {
642 canonical_smt_input_v0(
643 "scope-flatten-candidate",
644 "prove_scope_flatten_candidate",
645 vec![
646 smt_ir_computed_requirement_v0("no-limit-selector", input.limit_selector.is_none()),
647 smt_ir_computed_requirement_v0("root-scope", input.root_selector.trim() == ":root"),
648 smt_ir_computed_requirement_v0("no-peer-scope", input.peer_scope_count == 0),
649 smt_ir_computed_requirement_v0(
650 "no-competing-unscoped-rule",
651 input.competing_unscoped_rule_count == 0,
652 ),
653 smt_ir_computed_requirement_v0("not-inside-layer", !input.inside_layer),
654 ],
655 )
656}
657
658fn canonical_layer_flatten_candidate_input_v0(input: &LayerFlattenInputV0) -> CanonicalSmtInputV0 {
659 canonical_smt_input_v0(
660 "layer-flatten-candidate",
661 "prove_layer_flatten_candidate",
662 vec![
663 smt_producer_pass_through_requirement_v0("closed-bundle", input.closed_bundle),
664 smt_ir_computed_requirement_v0("no-peer-layer", input.peer_layer_count == 0),
665 smt_ir_computed_requirement_v0("no-unlayered-rule", input.unlayered_rule_count == 0),
666 smt_ir_computed_requirement_v0(
667 "no-important-declaration",
668 input.important_declaration_count == 0,
669 ),
670 ],
671 )
672}
673
674fn canonical_static_supports_condition_input_v0(
675 verdict: &StaticSupportsEvalVerdictV0,
676) -> CanonicalSmtInputV0 {
677 let canonical_terms = match verdict {
678 StaticSupportsEvalVerdictV0::AlwaysTrue => {
679 vec![smt_ir_computed_requirement_v0(
680 "supports-condition-known-true",
681 true,
682 )]
683 }
684 StaticSupportsEvalVerdictV0::AlwaysFalse => {
685 vec![smt_ir_computed_requirement_v0(
686 "supports-condition-known-true",
687 false,
688 )]
689 }
690 StaticSupportsEvalVerdictV0::Unknown => vec!["unknown:supports-condition".to_string()],
691 };
692 canonical_smt_input_v0(
693 "static-supports-condition",
694 "evaluate_static_supports_condition",
695 canonical_terms,
696 )
697}
698
699fn canonical_transform_rewrite_candidate_input_v0(
700 input: &TransformRewriteProofInputV0,
701) -> CanonicalSmtInputV0 {
702 canonical_smt_input_v0(
703 "transform-rewrite-candidate",
704 "verify_transform_rewrite_candidate",
705 vec![
706 format!("pass:{}", input.pass_id),
707 smt_producer_pass_through_requirement_v0(
708 "cascade-obligation-declared",
709 input.cascade_obligation_declared,
710 ),
711 smt_ir_computed_requirement_v0("provenance-recomputed", input.provenance_recomputed),
712 smt_ir_computed_requirement_v0("provenance-preserved", input.provenance_preserved),
713 smt_ir_computed_requirement_v0("no-bogus-or-trivia", !input.contains_bogus_or_trivia),
714 smt_ir_computed_requirement_v0(
715 "stable-post-semantic-ir",
716 input.stable_post_semantic_ir,
717 ),
718 ],
719 )
720}
721
722fn smt_require_term_v0(name: &str, value: bool) -> String {
723 format!("require:{name}={value}")
724}
725
726fn smt_ir_computed_requirement_v0(name: &str, value: bool) -> String {
729 smt_require_term_v0(name, value)
730}
731
732fn smt_producer_pass_through_requirement_v0(name: &str, value: bool) -> String {
737 smt_require_term_v0(name, value)
738}
739
740fn smt_box_shorthand_longhands_v0(shorthand_property: &str) -> Option<[&'static str; 4]> {
741 match shorthand_property {
742 "margin" => Some(["margin-top", "margin-right", "margin-bottom", "margin-left"]),
743 "padding" => Some([
744 "padding-top",
745 "padding-right",
746 "padding-bottom",
747 "padding-left",
748 ]),
749 "border-color" => Some([
750 "border-top-color",
751 "border-right-color",
752 "border-bottom-color",
753 "border-left-color",
754 ]),
755 "border-style" => Some([
756 "border-top-style",
757 "border-right-style",
758 "border-bottom-style",
759 "border-left-style",
760 ]),
761 "border-width" => Some([
762 "border-top-width",
763 "border-right-width",
764 "border-bottom-width",
765 "border-left-width",
766 ]),
767 "scroll-margin" => Some([
768 "scroll-margin-top",
769 "scroll-margin-right",
770 "scroll-margin-bottom",
771 "scroll-margin-left",
772 ]),
773 "scroll-padding" => Some([
774 "scroll-padding-top",
775 "scroll-padding-right",
776 "scroll-padding-bottom",
777 "scroll-padding-left",
778 ]),
779 _ => None,
780 }
781}
782
783#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
784#[serde(rename_all = "camelCase")]
785pub struct LayerInversionDeclarationV0 {
786 pub schema_version: &'static str,
787 pub product: &'static str,
788 pub layer_marker: &'static str,
789 pub feature_gate: &'static str,
790 pub declaration_id: String,
791 pub layer_rank: i64,
792 pub source_order: i64,
793}
794
795pub fn layer_inversion_declaration_v0(
796 declaration_id: impl Into<String>,
797 layer_rank: i64,
798 source_order: i64,
799) -> LayerInversionDeclarationV0 {
800 LayerInversionDeclarationV0 {
801 schema_version: SMT_SCHEMA_VERSION_V0,
802 product: "omena-smt.layer-inversion-declaration",
803 layer_marker: SMT_LAYER_MARKER_V0,
804 feature_gate: SMT_FEATURE_GATE_V0,
805 declaration_id: declaration_id.into(),
806 layer_rank,
807 source_order,
808 }
809}
810
811#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
812#[serde(rename_all = "camelCase")]
813pub struct LayerFlattenInversionVerdictV0 {
814 pub schema_version: &'static str,
815 pub product: &'static str,
816 pub layer_marker: &'static str,
817 pub feature_gate: &'static str,
818 pub backend: SmtBackendKindV0,
819 pub inversion_exists: bool,
820 pub verdict: SmtVerdictV0,
821 pub canonical_input: CanonicalSmtInputV0,
822 pub sat_result: SmtBackendSatResultV0,
823}
824
825impl LayerFlattenInversionVerdictV0 {
826 pub fn evidence_node_key(&self) -> EvidenceNodeKeyV0 {
827 EvidenceNodeKeyV0::new(
828 CASCADE_PROOF_RECORD_EVIDENCE_QUERY_V0,
829 self.canonical_input.obligation_id.clone(),
830 )
831 }
832
833 pub fn evidence_node_seed(&self) -> EvidenceNodeSeedV0 {
834 let provenance = vec![
835 ["obligation:", self.canonical_input.obligation_id.as_str()].concat(),
836 ["primitive:", self.canonical_input.l1_primitive].concat(),
837 ["featureGate:", self.feature_gate].concat(),
838 ];
839 let lookup = lookup_discharge_ledger_entry_v0(&self.canonical_input);
840 if let Some(seed) = ledger_backed_cascade_proof_seed_v0(
841 self.evidence_node_key(),
842 provenance.clone(),
843 &lookup,
844 ) {
845 return seed;
846 }
847 let family_stamp = prose_obligation_family_stamp(&provenance);
848 EvidenceNodeSeedV0::with_family(
849 self.evidence_node_key(),
850 provenance,
851 GuaranteeKindV0::for_label_less_family(),
852 family_stamp,
853 )
854 }
855
856 pub fn evidence_graph(&self) -> Result<EvidenceGraphV0, EvidenceGraphBuildErrorV0> {
857 build_evidence_graph_from_edges_v0(
858 [self.evidence_node_seed()],
859 [EvidenceDemandEdgeV0::new(
860 CASCADE_PROOF_RECORD_EVIDENCE_QUERY_V0,
861 self.evidence_node_key(),
862 CASCADE_PROOF_EVIDENCE_EDGE_KIND_V0,
863 )],
864 )
865 }
866}
867
868pub fn canonical_layer_flatten_inversion_input_v0(
869 declarations: &[LayerInversionDeclarationV0],
870) -> CanonicalSmtInputV0 {
871 let declarations = canonicalize_layer_inversion_declarations_v0(declarations);
872 let mut script = String::from("(set-logic QF_LIA)\n");
873 for (index, declaration) in declarations.iter().enumerate() {
874 script.push_str(&format!("(declare-const rank_{index} Int)\n"));
875 script.push_str(&format!("(declare-const source_{index} Int)\n"));
876 script.push_str(&format!(
877 "(assert (= rank_{index} {}))\n",
878 smtlib2_int_v0(declaration.layer_rank)
879 ));
880 script.push_str(&format!(
881 "(assert (= source_{index} {}))\n",
882 smtlib2_int_v0(declaration.source_order)
883 ));
884 }
885
886 let mut inversion_clauses = Vec::new();
887 for a in 0..declarations.len() {
888 for b in 0..declarations.len() {
889 if a == b {
890 continue;
891 }
892 inversion_clauses.push(format!(
893 "(and (> rank_{a} rank_{b}) (> source_{b} source_{a}))"
894 ));
895 }
896 }
897
898 let inversion_assertion = match inversion_clauses.len() {
899 0 => "false".to_string(),
900 1 => inversion_clauses.remove(0),
901 _ => format!("(or {})", inversion_clauses.join(" ")),
902 };
903 script.push_str(&format!(
904 "(assert (! {inversion_assertion} :named cascade_layer_flatten_inversion))\n"
905 ));
906
907 let canonical_terms = declarations
908 .iter()
909 .map(|declaration| {
910 format!(
911 "decl:{}:rank={}:source={}",
912 declaration.declaration_id, declaration.layer_rank, declaration.source_order
913 )
914 })
915 .collect();
916
917 canonical_smt_input_with_script_v0(
918 "layer-flatten-cascade-inversion",
919 "prove_layer_flatten_candidate",
920 canonical_terms,
921 script,
922 )
923}
924
925pub fn canonicalize_layer_inversion_declarations_v0(
926 declarations: &[LayerInversionDeclarationV0],
927) -> Vec<LayerInversionDeclarationV0> {
928 let mut layer_ranks = declarations
929 .iter()
930 .map(|declaration| declaration.layer_rank)
931 .collect::<Vec<_>>();
932 layer_ranks.sort_unstable();
933 layer_ranks.dedup();
934 let mut source_orders = declarations
935 .iter()
936 .map(|declaration| declaration.source_order)
937 .collect::<Vec<_>>();
938 source_orders.sort_unstable();
939 source_orders.dedup();
940
941 declarations
942 .iter()
943 .enumerate()
944 .map(|(index, declaration)| {
945 layer_inversion_declaration_v0(
946 format!("decl-{index}"),
947 ordinal_coordinate(declaration.layer_rank, &layer_ranks),
948 ordinal_coordinate(declaration.source_order, &source_orders),
949 )
950 })
951 .collect()
952}
953
954fn ordinal_coordinate(value: i64, ordered_values: &[i64]) -> i64 {
955 let index = ordered_values
956 .binary_search(&value)
957 .unwrap_or_else(|index| index) as i64;
958 index - (ordered_values.len().saturating_sub(1) as i64 / 2)
959}
960
961pub fn smt_check_layer_flatten_inversion_v0<B: SmtBackendV0>(
962 declarations: &[LayerInversionDeclarationV0],
963 backend: &B,
964) -> LayerFlattenInversionVerdictV0 {
965 let canonical_input = canonical_layer_flatten_inversion_input_v0(declarations);
966 let check = backend.check_canonical_input_v0(&canonical_input);
967 let inversion_exists = matches!(check.sat_result, SmtBackendSatResultV0::Sat);
968 let verdict = match check.sat_result {
969 SmtBackendSatResultV0::Sat => SmtVerdictV0::Rejected,
970 SmtBackendSatResultV0::Unsat => SmtVerdictV0::Accepted,
971 SmtBackendSatResultV0::Unknown => SmtVerdictV0::Unknown,
972 };
973 LayerFlattenInversionVerdictV0 {
974 schema_version: SMT_SCHEMA_VERSION_V0,
975 product: "omena-smt.layer-flatten-inversion",
976 layer_marker: SMT_LAYER_MARKER_V0,
977 feature_gate: SMT_FEATURE_GATE_V0,
978 backend: backend.backend_kind(),
979 inversion_exists,
980 verdict,
981 canonical_input,
982 sat_result: check.sat_result,
983 }
984}
985
986fn smtlib2_int_v0(value: i64) -> String {
987 if value < 0 {
988 format!("(- {})", value.unsigned_abs())
989 } else {
990 value.to_string()
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 use super::*;
997 use omena_cascade::{
998 StaticSupportsEvalVerdictV0, evaluate_static_supports_condition,
999 prove_box_shorthand_combination, prove_layer_flatten_candidate,
1000 prove_scope_flatten_candidate,
1001 };
1002 use omena_evidence_graph::GuaranteeFamilyV0;
1003
1004 fn accepted_verdict(accepted: bool) -> SmtVerdictV0 {
1005 if accepted {
1006 SmtVerdictV0::Accepted
1007 } else {
1008 SmtVerdictV0::Rejected
1009 }
1010 }
1011
1012 #[test]
1013 fn requirement_origin_labels_preserve_wire_and_non_require_terms_are_neutral() {
1014 assert_eq!(
1015 smt_ir_computed_requirement_v0("from-ir", true),
1016 "require:from-ir=true"
1017 );
1018 assert_eq!(
1019 smt_producer_pass_through_requirement_v0("from-producer", false),
1020 "require:from-producer=false"
1021 );
1022
1023 let neutral_audit_label = canonical_smt_input_v0(
1024 "classification-control",
1025 "classification_control",
1026 vec![
1027 smt_ir_computed_requirement_v0("from-ir", true),
1028 "pass:not-a-requirement".to_owned(),
1029 ],
1030 );
1031 let rejected_pass_through = canonical_smt_input_v0(
1032 "classification-negative",
1033 "classification_control",
1034 vec![smt_producer_pass_through_requirement_v0(
1035 "from-producer",
1036 false,
1037 )],
1038 );
1039 let backend = StubSmtBackendV0::default();
1040
1041 assert_eq!(
1042 backend
1043 .check_canonical_input_v0(&neutral_audit_label)
1044 .sat_result,
1045 SmtBackendSatResultV0::Sat
1046 );
1047 assert_eq!(
1048 backend
1049 .check_canonical_input_v0(&rejected_pass_through)
1050 .sat_result,
1051 SmtBackendSatResultV0::Unsat
1052 );
1053 }
1054
1055 #[test]
1056 fn default_backend_matches_l1_box_shorthand_verdict() {
1057 let backend = StubSmtBackendV0::default();
1058 let proof = smt_prove_box_shorthand_combination_v0(
1059 "margin",
1060 &[
1061 BoxLonghandInputV0 {
1062 property: "margin-top".to_string(),
1063 value: "1px".to_string(),
1064 important: false,
1065 source_order: 1,
1066 },
1067 BoxLonghandInputV0 {
1068 property: "margin-right".to_string(),
1069 value: "1px".to_string(),
1070 important: false,
1071 source_order: 2,
1072 },
1073 BoxLonghandInputV0 {
1074 property: "margin-bottom".to_string(),
1075 value: "1px".to_string(),
1076 important: false,
1077 source_order: 3,
1078 },
1079 BoxLonghandInputV0 {
1080 property: "margin-left".to_string(),
1081 value: "1px".to_string(),
1082 important: false,
1083 source_order: 4,
1084 },
1085 ],
1086 &backend,
1087 );
1088 assert_eq!(proof.schema_version, "0");
1089 assert_eq!(proof.verdict, SmtVerdictV0::Accepted);
1090 assert_eq!(proof.backend, SmtBackendKindV0::Stub);
1091 assert!(
1092 proof
1093 .canonical_input
1094 .smtlib2_script
1095 .contains("(set-logic QF_UF)")
1096 );
1097 }
1098
1099 #[test]
1100 fn transform_rewrite_verification_runs_backend_check() {
1101 let backend = StubSmtBackendV0::default();
1102 let proof_input = TransformRewriteProofInputV0::new(
1103 "rule-deduplication",
1104 ObligationFamilyIdV0::CascadeObligationDeclaration,
1105 true,
1106 true,
1107 false,
1108 true,
1109 );
1110 let proof = smt_verify_transform_rewrite_candidate_v0(&proof_input, &backend);
1111
1112 assert_eq!(proof.verdict, SmtVerdictV0::Accepted);
1113 assert_eq!(proof.l1_primitive, "verify_transform_rewrite_candidate");
1114 assert_eq!(
1115 proof.canonical_input.obligation_id,
1116 "transform-rewrite-candidate"
1117 );
1118 assert!(
1119 proof
1120 .canonical_input
1121 .canonical_terms
1122 .contains(&"require:provenance-recomputed=true".to_string())
1123 );
1124 assert!(
1125 proof
1126 .canonical_input
1127 .canonical_terms
1128 .contains(&"require:no-bogus-or-trivia=true".to_string())
1129 );
1130 }
1131
1132 #[test]
1133 fn proof_style_bisimulation_invariant_holds_for_all_l1_primitives() {
1134 let backend = StubSmtBackendV0::default();
1135 let longhands = vec![
1136 BoxLonghandInputV0 {
1137 property: "margin-top".to_string(),
1138 value: "1px".to_string(),
1139 important: false,
1140 source_order: 1,
1141 },
1142 BoxLonghandInputV0 {
1143 property: "margin-right".to_string(),
1144 value: "1px".to_string(),
1145 important: false,
1146 source_order: 2,
1147 },
1148 BoxLonghandInputV0 {
1149 property: "margin-bottom".to_string(),
1150 value: "1px".to_string(),
1151 important: false,
1152 source_order: 3,
1153 },
1154 BoxLonghandInputV0 {
1155 property: "margin-left".to_string(),
1156 value: "1px".to_string(),
1157 important: false,
1158 source_order: 4,
1159 },
1160 ];
1161 let l1_box = prove_box_shorthand_combination("margin", &longhands);
1162 let l3_box = smt_prove_box_shorthand_combination_v0("margin", &longhands, &backend);
1163 assert_eq!(l3_box.verdict, accepted_verdict(l1_box.accepted));
1164
1165 let scope_input = ScopeFlattenInputV0 {
1166 root_selector: ":root".to_string(),
1167 limit_selector: None,
1168 scoped_rule_count: 1,
1169 peer_scope_count: 0,
1170 competing_unscoped_rule_count: 0,
1171 inside_layer: false,
1172 };
1173 let l1_scope = prove_scope_flatten_candidate(scope_input.clone());
1174 let l3_scope = smt_prove_scope_flatten_candidate_v0(scope_input, &backend);
1175 assert_eq!(l3_scope.verdict, accepted_verdict(l1_scope.accepted));
1176
1177 let layer_input = LayerFlattenInputV0 {
1178 layer_name: Some("components".to_string()),
1179 layer_rule_count: 1,
1180 peer_layer_count: 0,
1181 unlayered_rule_count: 0,
1182 important_declaration_count: 0,
1183 closed_bundle: true,
1184 };
1185 let l1_layer = prove_layer_flatten_candidate(layer_input.clone());
1186 let l3_layer = smt_prove_layer_flatten_candidate_v0(layer_input, &backend);
1187 assert_eq!(l3_layer.verdict, accepted_verdict(l1_layer.accepted));
1188 }
1189
1190 #[test]
1191 fn static_supports_smt_equivalence_tracks_l1_verdict_shape() {
1192 let backend = StubSmtBackendV0::default();
1193 let l1 = evaluate_static_supports_condition(
1194 "(display: grid)",
1195 StaticSupportsAssumptionV0::ModernBrowser,
1196 );
1197 let l3 = smt_evaluate_static_supports_condition_v0(
1198 "(display: grid)",
1199 StaticSupportsAssumptionV0::ModernBrowser,
1200 &backend,
1201 );
1202
1203 assert_eq!(l1.verdict, StaticSupportsEvalVerdictV0::AlwaysTrue);
1204 assert_eq!(l3.verdict, SmtVerdictV0::Accepted);
1205 assert_eq!(l3.l1_primitive, "evaluate_static_supports_condition");
1206 }
1207
1208 #[test]
1209 fn smt_bisimulation_fuzz_seed_corpus_covers_fixture_shapes() {
1210 let report = run_smt_bisimulation_fuzz_seed_corpus_v0(128);
1211 assert_eq!(report.schema_version, "0");
1212 assert_eq!(report.fixture_suite, "m3-cascade-proof-fixtures");
1213 assert_eq!(report.checked_obligation_count, 128 * 4);
1214 assert_eq!(report.l1_l3_mismatch_count, 0);
1215 assert!(report.passed);
1216 }
1217
1218 #[test]
1219 fn smt_bisimulation_fuzz_case_is_a_schema_zero_contract() {
1220 let case = smt_bisimulation_fuzz_case_v0(42);
1221 assert_eq!(case.schema_version, "0");
1222 assert_eq!(case.layer_marker, "smt-cascade-verification");
1223 assert_eq!(case.feature_gate, "smt-stub");
1224 assert_eq!(case.seed, 42);
1225 }
1226
1227 #[test]
1228 fn rewrite_proof_input_evidence_graph_preserves_public_shape() -> Result<(), serde_json::Error>
1229 {
1230 let input = TransformRewriteProofInputV0::new(
1231 "number-compression",
1232 ObligationFamilyIdV0::CascadeObligationDeclaration,
1233 true,
1234 true,
1235 false,
1236 true,
1237 );
1238
1239 let before = serde_json::to_value(&input)?;
1240 let graph = input
1241 .evidence_graph()
1242 .map_err(|_| serde::ser::Error::custom("input edge must target its node"))?;
1243 let after = serde_json::to_value(&input)?;
1244
1245 assert_eq!(before, after);
1246 assert_eq!(graph.nodes.len(), 1);
1247 assert_eq!(graph.nodes[0].key.input_identity, "number-compression");
1248 assert_eq!(graph.nodes[0].guarantee, GuaranteeKindV0::Floor);
1249 assert_eq!(
1250 graph.nodes[0].earned_via(),
1251 GuaranteeFamilyV0::ProseObligationDischarged
1252 );
1253 assert!(
1254 graph.nodes[0]
1255 .provenance
1256 .iter()
1257 .any(|item| item == "provenancePreserved:true")
1258 );
1259 Ok(())
1260 }
1261
1262 #[test]
1263 fn rewrite_proof_input_family_derivation_preserves_legacy_json_contract()
1264 -> Result<(), serde_json::Error> {
1265 for (pass_id, family, expected_declared) in [
1266 (
1267 "number-compression",
1268 ObligationFamilyIdV0::CascadeObligationDeclaration,
1269 true,
1270 ),
1271 ("print-css", ObligationFamilyIdV0::CascadeSafetyFloor, false),
1272 ] {
1273 let input =
1274 TransformRewriteProofInputV0::new(pass_id, family, true, false, false, true);
1275
1276 assert_eq!(
1277 serde_json::to_value(&input)?,
1278 serde_json::json!({
1279 "schemaVersion": "0",
1280 "product": "omena-cascade-proof.transform-rewrite-input",
1281 "passId": pass_id,
1282 "cascadeObligationDeclared": expected_declared,
1283 "provenanceRecomputed": true,
1284 "provenancePreserved": false,
1285 "containsBogusOrTrivia": false,
1286 "stablePostSemanticIr": true,
1287 })
1288 );
1289 assert_eq!(
1290 serde_json::to_value(input.evidence_node_seed())?,
1291 serde_json::json!({
1292 "key": {
1293 "queryIdentity": REWRITE_PROOF_INPUT_EVIDENCE_QUERY_V0,
1294 "inputIdentity": pass_id,
1295 },
1296 "provenance": [
1297 format!("pass:{pass_id}"),
1298 format!("cascadeObligationDeclared:{expected_declared}"),
1299 "provenanceRecomputed:true",
1300 "provenancePreserved:false",
1301 ],
1302 "guarantee": "floor",
1303 "earnedVia": "proseObligationDischarged",
1304 })
1305 );
1306 }
1307
1308 Ok(())
1309 }
1310
1311 #[test]
1312 fn cascade_proof_record_evidence_graph_preserves_public_shape() -> Result<(), serde_json::Error>
1313 {
1314 let backend = StubSmtBackendV0::default();
1315 let proof = smt_verify_transform_rewrite_candidate_v0(
1316 &TransformRewriteProofInputV0::new(
1317 "number-compression",
1318 ObligationFamilyIdV0::CascadeObligationDeclaration,
1319 true,
1320 true,
1321 false,
1322 true,
1323 ),
1324 &backend,
1325 );
1326
1327 let before = serde_json::to_value(&proof)?;
1328 let graph = proof
1329 .evidence_graph()
1330 .map_err(|_| serde::ser::Error::custom("proof edge must target its node"))?;
1331 let after = serde_json::to_value(&proof)?;
1332
1333 assert_eq!(before, after);
1334 assert_eq!(graph.nodes.len(), 1);
1335 assert_eq!(graph.nodes[0].key.input_identity, proof.obligation_id);
1336 assert_eq!(graph.nodes[0].guarantee, GuaranteeKindV0::Floor);
1337 assert_eq!(
1338 graph.nodes[0].earned_via(),
1339 GuaranteeFamilyV0::ProseObligationDischarged
1340 );
1341 assert!(
1342 graph.nodes[0]
1343 .provenance
1344 .iter()
1345 .any(|item| item == "primitive:verify_transform_rewrite_candidate")
1346 );
1347 Ok(())
1348 }
1349
1350 #[test]
1351 fn cascade_proof_record_uses_ledger_family_on_matching_cell() -> Result<(), serde_json::Error> {
1352 let backend = StubSmtBackendV0::default();
1353 let longhands = vec![
1354 LonghandMergeInputV0 {
1355 property: "margin-top".to_string(),
1356 value: "1px".to_string(),
1357 important: false,
1358 source_order: 1,
1359 },
1360 LonghandMergeInputV0 {
1361 property: "margin-right".to_string(),
1362 value: "1px".to_string(),
1363 important: false,
1364 source_order: 2,
1365 },
1366 LonghandMergeInputV0 {
1367 property: "margin-bottom".to_string(),
1368 value: "1px".to_string(),
1369 important: false,
1370 source_order: 3,
1371 },
1372 LonghandMergeInputV0 {
1373 property: "margin-left".to_string(),
1374 value: "1px".to_string(),
1375 important: false,
1376 source_order: 4,
1377 },
1378 ];
1379 let proof = smt_prove_longhand_merge_v0(
1380 "margin",
1381 &["margin-top", "margin-right", "margin-bottom", "margin-left"],
1382 &longhands,
1383 &backend,
1384 );
1385 let graph = proof
1386 .evidence_graph()
1387 .map_err(|_| serde::ser::Error::custom("proof edge must target its node"))?;
1388
1389 assert_eq!(graph.nodes.len(), 1);
1390 assert_eq!(graph.nodes[0].guarantee, GuaranteeKindV0::Floor);
1391 assert_eq!(
1392 graph.nodes[0].earned_via(),
1393 GuaranteeFamilyV0::LedgerBackedObligationDischarge
1394 );
1395 assert!(
1396 graph.nodes[0]
1397 .provenance
1398 .iter()
1399 .any(|item| item.starts_with("dischargeCell:"))
1400 );
1401 Ok(())
1402 }
1403}