From d738638d17e9e6f4aa85ae30514bc8e97c5f937c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 23:18:20 +0000 Subject: [PATCH 1/2] pearl::hydrate: a binding counts only under the relation's own predicate `hydrate` located the intermediate `B` of `A -p-> Y` by endpoints alone: any sealed step `A -> B` plus any `B -> Y`, under whatever predicate, moved the topology from `IndirectUnknown` to `IndirectKnown`. A chain recorded under another predicate is another chain; it says nothing about where this relation's missing link sits. The match now requires the step's predicate ordinal to equal the hydrated edge's `p_idx` before the endpoints are compared. Foreign-predicate steps are not counted at all, not even as `Partial`. Falsifier `hydration_counts_only_bindings_under_the_relations_own_predicate`: both legs foreign -> `ProposedOnly`; one leg foreign -> `Partial`, topology unchanged; silence twin: same-predicate legs beside foreign ones still hydrate. Red before the change (returned `Hydrated`), green after. Gates: planner lib 462 passed; `self_orchestration_probe` 15, `staunen_scheduler_probe` 23, `pearl_ladder_probe` 4; clippy -D warnings clean on the lib (pre-existing example lints untouched). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_017SNHxJYC1MeW3HsyhGZo1E --- crates/lance-graph-planner/src/pearl.rs | 61 +++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/crates/lance-graph-planner/src/pearl.rs b/crates/lance-graph-planner/src/pearl.rs index 7786dff5b..e2c17be56 100644 --- a/crates/lance-graph-planner/src/pearl.rs +++ b/crates/lance-graph-planner/src/pearl.rs @@ -27,7 +27,8 @@ //! the certification, not certifications. //! - **Topology changes only through [`hydrate`]**: `IndirectUnknown` becomes //! `IndirectKnown` when both bindings `A → B` and `B → Y` exist in the sealed -//! chain. Proposing a candidate is not evidence. +//! chain under the relation's own predicate. Proposing a candidate is not +//! evidence; a chain under another predicate is another chain. //! - **Only bits 59..63 move.** The projection, mantissa, frequency and //! confidence of the edge are inputs, never outputs. //! - **A pass cannot be asserted.** [`Measured`] has private fields; only @@ -435,8 +436,11 @@ pub enum Hydration { } /// The topology gate. `IndirectUnknown` becomes `IndirectKnown` only when both -/// bindings `a → b` and `b → y` are present in the sealed chain. Certification -/// never moves here. +/// bindings `a → b` and `b → y` are present in the sealed chain **under the +/// hydrated relation's own predicate** (the step's ordinal equals the edge's +/// `p_idx`). A chain recorded under another predicate is another chain; it +/// locates nothing about this relation's intermediate and is not counted, +/// not even as `Partial`. Certification never moves here. /// /// # Errors /// @@ -447,7 +451,14 @@ pub fn hydrate( sealed: &[ChainStep], reading: Reading<'_>, ) -> Result<(CausalEdge64, Hydration), Epi5ReadError> { - let has = |s: u8, o: u8| sealed.iter().any(|(_, e)| e.s_idx() == s && e.o_idx() == o); + // A binding is a step of THIS relation's chain: same predicate ordinal, + // then the endpoints. Steps under another predicate are another chain. + let p = edge.p_idx(); + let has = |s: u8, o: u8| { + sealed + .iter() + .any(|(pred, e)| *pred == p && e.s_idx() == s && e.o_idx() == o) + }; let found = match (has(a, b), has(b, y)) { (true, true) => Hydration::Hydrated, (false, false) => Hydration::ProposedOnly, @@ -1285,6 +1296,48 @@ mod tests { assert_eq!(state(out).0, Topology2::IndirectKnown); } + /// A binding counts only under the hydrated relation's own predicate. + /// `A → B` and `B → Y` recorded under another predicate locate nothing + /// about `A -p-> Y`; a chain under a different predicate is a different + /// chain, not a located intermediate of this one. + #[test] + fn hydration_counts_only_bindings_under_the_relations_own_predicate() { + let d = declarations(); + const OTHER: u8 = PREDICATE ^ 0x3C; + let q = query( + CausalMask::SO, + Certification3::Related, + Topology2::IndirectUnknown, + ); + // Both legs exist, both under another predicate: nothing is bound. + let foreign: Vec = vec![ + (OTHER, edge(A, B, 40, 220, CausalMask::SPO)), + (OTHER, edge(B, Y, 250, 220, CausalMask::SPO)), + ]; + assert_eq!( + hydrate(q, (A, B, Y), &foreign, reading(&d)).unwrap(), + (q, Hydration::ProposedOnly), + "a chain under another predicate is not this relation's chain" + ); + // One leg under the relation's predicate, the other under a foreign + // one: half a link, and the topology stays put. + let mixed: Vec = vec![ + (PREDICATE, edge(A, B, 40, 220, CausalMask::SPO)), + (OTHER, edge(B, Y, 250, 220, CausalMask::SPO)), + ]; + assert_eq!( + hydrate(q, (A, B, Y), &mixed, reading(&d)).unwrap(), + (q, Hydration::Partial) + ); + // Silence twin: both legs under the relation's predicate hydrate, + // and the foreign steps beside them change nothing. + let mut both = foreign.clone(); + both.extend(chain_steps()); + let (e, h) = hydrate(q, (A, B, Y), &both, reading(&d)).unwrap(); + assert_eq!(h, Hydration::Hydrated); + assert_eq!(state(e).0, Topology2::IndirectKnown); + } + #[test] fn a_cut_that_moves_nothing_is_inert() { // CodeRabbit #1391: classification used to compare the arms' lengths, From c48ad7f83281062883b3c9541c5e5cf541562e04 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 23:24:46 +0000 Subject: [PATCH 2/2] Revert "pearl::hydrate: a binding counts only under the relation's own predicate" This reverts commit d738638d17e9e6f4aa85ae30514bc8e97c5f937c. --- crates/lance-graph-planner/src/pearl.rs | 61 ++----------------------- 1 file changed, 4 insertions(+), 57 deletions(-) diff --git a/crates/lance-graph-planner/src/pearl.rs b/crates/lance-graph-planner/src/pearl.rs index e2c17be56..7786dff5b 100644 --- a/crates/lance-graph-planner/src/pearl.rs +++ b/crates/lance-graph-planner/src/pearl.rs @@ -27,8 +27,7 @@ //! the certification, not certifications. //! - **Topology changes only through [`hydrate`]**: `IndirectUnknown` becomes //! `IndirectKnown` when both bindings `A → B` and `B → Y` exist in the sealed -//! chain under the relation's own predicate. Proposing a candidate is not -//! evidence; a chain under another predicate is another chain. +//! chain. Proposing a candidate is not evidence. //! - **Only bits 59..63 move.** The projection, mantissa, frequency and //! confidence of the edge are inputs, never outputs. //! - **A pass cannot be asserted.** [`Measured`] has private fields; only @@ -436,11 +435,8 @@ pub enum Hydration { } /// The topology gate. `IndirectUnknown` becomes `IndirectKnown` only when both -/// bindings `a → b` and `b → y` are present in the sealed chain **under the -/// hydrated relation's own predicate** (the step's ordinal equals the edge's -/// `p_idx`). A chain recorded under another predicate is another chain; it -/// locates nothing about this relation's intermediate and is not counted, -/// not even as `Partial`. Certification never moves here. +/// bindings `a → b` and `b → y` are present in the sealed chain. Certification +/// never moves here. /// /// # Errors /// @@ -451,14 +447,7 @@ pub fn hydrate( sealed: &[ChainStep], reading: Reading<'_>, ) -> Result<(CausalEdge64, Hydration), Epi5ReadError> { - // A binding is a step of THIS relation's chain: same predicate ordinal, - // then the endpoints. Steps under another predicate are another chain. - let p = edge.p_idx(); - let has = |s: u8, o: u8| { - sealed - .iter() - .any(|(pred, e)| *pred == p && e.s_idx() == s && e.o_idx() == o) - }; + let has = |s: u8, o: u8| sealed.iter().any(|(_, e)| e.s_idx() == s && e.o_idx() == o); let found = match (has(a, b), has(b, y)) { (true, true) => Hydration::Hydrated, (false, false) => Hydration::ProposedOnly, @@ -1296,48 +1285,6 @@ mod tests { assert_eq!(state(out).0, Topology2::IndirectKnown); } - /// A binding counts only under the hydrated relation's own predicate. - /// `A → B` and `B → Y` recorded under another predicate locate nothing - /// about `A -p-> Y`; a chain under a different predicate is a different - /// chain, not a located intermediate of this one. - #[test] - fn hydration_counts_only_bindings_under_the_relations_own_predicate() { - let d = declarations(); - const OTHER: u8 = PREDICATE ^ 0x3C; - let q = query( - CausalMask::SO, - Certification3::Related, - Topology2::IndirectUnknown, - ); - // Both legs exist, both under another predicate: nothing is bound. - let foreign: Vec = vec![ - (OTHER, edge(A, B, 40, 220, CausalMask::SPO)), - (OTHER, edge(B, Y, 250, 220, CausalMask::SPO)), - ]; - assert_eq!( - hydrate(q, (A, B, Y), &foreign, reading(&d)).unwrap(), - (q, Hydration::ProposedOnly), - "a chain under another predicate is not this relation's chain" - ); - // One leg under the relation's predicate, the other under a foreign - // one: half a link, and the topology stays put. - let mixed: Vec = vec![ - (PREDICATE, edge(A, B, 40, 220, CausalMask::SPO)), - (OTHER, edge(B, Y, 250, 220, CausalMask::SPO)), - ]; - assert_eq!( - hydrate(q, (A, B, Y), &mixed, reading(&d)).unwrap(), - (q, Hydration::Partial) - ); - // Silence twin: both legs under the relation's predicate hydrate, - // and the foreign steps beside them change nothing. - let mut both = foreign.clone(); - both.extend(chain_steps()); - let (e, h) = hydrate(q, (A, B, Y), &both, reading(&d)).unwrap(); - assert_eq!(h, Hydration::Hydrated); - assert_eq!(state(e).0, Topology2::IndirectKnown); - } - #[test] fn a_cut_that_moves_nothing_is_inert() { // CodeRabbit #1391: classification used to compare the arms' lengths,