Pin the sibling-order admissibility checks with direct tests
The round-trip property found these paths originally, but no
deterministic test held them: mutating admissible's && to || and
sibling_order_holds's != to == both survived a full cargo-mutants
run under the timeout exit code. The crossing case exercises the
asymmetry — an image that is not a sibling of the candidate on the
other side — so only one of the two sibling-order calls can see it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01565B66CMh3HTjm3124SLpB
change
commit 01c449da2cb8840cb24481ab0621e2c8b1bdb241
author Claude <noreply@anthropic.com>
date
parent rmkvwlsn
diff --git a/src/diff.rs b/src/diff.rs
index 4a7fd12..1ee5f91 100644
--- a/src/diff.rs
+++ b/src/diff.rs
@@ -894,6 +894,44 @@ mod tests {
         Ok(())
     }
 
+    #[test]
+    fn candidate_with_a_crossing_sibling_image_is_inadmissible() -> Result<(), Error> {
+        // src's "1" sits before the candidate "2", but its image lands
+        // after cy inside a nested array — a crossing only the src-side
+        // sibling-order check can see, since the image is not a sibling
+        // of cy on the dst side.
+        let src = parse_json("[1, 2]")?;
+        let dst = parse_json("[2, [1]]")?;
+        let (Some(s1), Some(d1)) = (find_number(&src, "1"), find_number(&dst, "1")) else {
+            unreachable!("both sources hold a number 1");
+        };
+        let (Some(cx), Some(cy)) = (find_number(&src, "2"), find_number(&dst, "2")) else {
+            unreachable!("both sources hold a number 2");
+        };
+        let mut m = Matching::new(&src, &dst);
+        m.insert(s1, d1);
+        assert!(!admissible(&src, &dst, &m, cx, cy));
+        Ok(())
+    }
+
+    #[test]
+    fn candidate_with_sibling_images_in_order_is_admissible() -> Result<(), Error> {
+        // Same shape as the crossing case, but the nested "1" stays on
+        // the same side of the candidate as its preimage.
+        let src = parse_json("[1, 2]")?;
+        let dst = parse_json("[[1], 2]")?;
+        let (Some(s1), Some(d1)) = (find_number(&src, "1"), find_number(&dst, "1")) else {
+            unreachable!("both sources hold a number 1");
+        };
+        let (Some(cx), Some(cy)) = (find_number(&src, "2"), find_number(&dst, "2")) else {
+            unreachable!("both sources hold a number 2");
+        };
+        let mut m = Matching::new(&src, &dst);
+        m.insert(s1, d1);
+        assert!(admissible(&src, &dst, &m, cx, cy));
+        Ok(())
+    }
+
     #[test]
     fn insert_relinks_stale_pairs() -> Result<(), Error> {
         // Bijectivity is structural: re-inserting either endpoint