From 6c4607d7f762188a0b76b8fca0bdde26c4aa5ee7 Mon Sep 17 00:00:00 2001 From: "Claude (Ada)" Date: Thu, 21 May 2026 10:25:50 +0000 Subject: [PATCH 1/2] feat(hpc): widen dn_tree::bundle_into + add OntologySchema::is_ancestor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two minimum-surface additions that enable Pillar-13 and Pillar-14 drift-check tests to compare the substrate-tier pillars (PR #188) against the production code paths they certify, without coupling production code to pillar code. src/hpc/dn_tree.rs - bundle_into: fn -> pub(crate) fn - No behavioural change; visibility only. - Lets crate::hpc::pillar::hhtl_contraction import the production bundle for cross-checking against its Bernoulli-mixture reference. src/hpc/ogit_bridge/schema.rs - New: OntologySchema::is_ancestor(&self, ancestor, descendant) -> bool - Walks the EntityClass.parent chain (rdfs:subClassOf), reflexive, with a defensive depth cap of 64 against any cycle that might slip past upstream antisymmetry checks. - Six unit tests: reflexivity, direct parent, transitive chain, antisymmetry, unknown descendant, disjoint chains. - Lets crate::hpc::pillar::ogit_lattice verify the same three partial-order axioms it certifies on synthetic DAGs also hold on the production schema's actual closure. Both additions are pub(crate) / pub-with-documented-instability — they do not commit to a long-term public API surface. The drift-check tests that depend on them live in crate::hpc::pillar::* and are gated under the existing pillar feature. Replies to https://github.com/AdaWorldAPI/ndarray/pull/188#issuecomment-4507158204 --- src/hpc/dn_tree.rs | 9 ++- src/hpc/ogit_bridge/schema.rs | 146 ++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/src/hpc/dn_tree.rs b/src/hpc/dn_tree.rs index 1f472583..573153e9 100644 --- a/src/hpc/dn_tree.rs +++ b/src/hpc/dn_tree.rs @@ -105,7 +105,14 @@ fn is_zero(hv: &GraphHV) -> bool { /// For each bit position, the existing summary bit is kept with probability /// `1 - lr * boost`, and replaced by the input bit with probability `lr * boost`. /// This is implemented per-word using a stochastic mask. -fn bundle_into(current: &GraphHV, hv: &GraphHV, lr: f64, boost: f64, rng: &mut SplitMix64) -> GraphHV { +/// +/// # Visibility +/// +/// Exposed as `pub(crate)` so the Pillar-13 drift-check test in +/// `crate::hpc::pillar::hhtl_contraction` can run the production bundle +/// against its independently-derived Bernoulli-mixture reference. The +/// function is not part of the public API and may change without notice. +pub(crate) fn bundle_into(current: &GraphHV, hv: &GraphHV, lr: f64, boost: f64, rng: &mut SplitMix64) -> GraphHV { let effective_lr = (lr * boost).min(1.0); let mut result = current.clone(); diff --git a/src/hpc/ogit_bridge/schema.rs b/src/hpc/ogit_bridge/schema.rs index 59361939..d10e1ae2 100644 --- a/src/hpc/ogit_bridge/schema.rs +++ b/src/hpc/ogit_bridge/schema.rs @@ -586,6 +586,72 @@ impl OntologySchema { leaf_count, }) } + + /// Returns `true` iff `descendant` is reachable from `ancestor` by + /// walking the `rdfs:subClassOf` chain encoded in + /// [`EntityClass::parent`]. The relation is *reflexive*: every class + /// is its own ancestor (including unknown IRIs — which return `true` + /// only for the reflexive case `ancestor == descendant`). + /// + /// # Complexity + /// + /// O(depth) — walks at most `MAX_DEPTH = 64` parent links before + /// returning `false`, defensively guarding against any cycle that + /// might have slipped past the partial-order check upstream. Pillar-14 + /// asserts the closure is antisymmetric on synthetic DAGs; this + /// method's cycle guard is the runtime backstop for that assertion. + /// + /// # Use case + /// + /// The Pillar-14 drift-check test in `crate::hpc::pillar::ogit_lattice` + /// uses this method to verify the loaded ontology's closure satisfies + /// the same three partial-order axioms the synthetic-DAG probe + /// certifies. Beyond that, it is the natural query for any + /// type-gated cascade step: "may activation propagate from a tile of + /// type `descendant` to a tile of type `ancestor`?" + /// + /// # Example + /// + /// ```ignore + /// // Reflexive + /// assert!(schema.is_ancestor("ogit:Heel", "ogit:Heel")); + /// // Transitive via the subClassOf chain + /// assert!(schema.is_ancestor("ogit:Heel", "ogit:SomeLeaf")); + /// // No relation + /// assert!(!schema.is_ancestor("ogit:Leaf", "ogit:Heel")); + /// // Unknown descendant — only reflexive case returns true + /// assert!(!schema.is_ancestor("ogit:Heel", "ogit:Unknown")); + /// ``` + pub fn is_ancestor(&self, ancestor: &str, descendant: &str) -> bool { + // Reflexive case — handles both the "X is its own ancestor" + // identity and the unknown-class case (where neither IRI is in + // the schema but they are equal). + if ancestor == descendant { + return true; + } + + // Walk the parent chain from descendant upward, looking for ancestor. + // Defensive depth cap — see method docstring. + const MAX_DEPTH: usize = 64; + let mut current: &str = descendant; + for _ in 0..MAX_DEPTH { + let entity = match self.entities.get(current) { + Some(e) => e, + None => return false, // descendant unknown — no chain to walk + }; + let parent = match entity.parent.as_deref() { + Some(p) => p, + None => return false, // reached root without finding ancestor + }; + if parent == ancestor { + return true; + } + current = parent; + } + // Exceeded depth cap — treat as not-an-ancestor (defensive; this + // path should be unreachable on a well-formed schema). + false + } } // --------------------------------------------------------------------------- @@ -775,4 +841,84 @@ mod tests { // Both bits must be set assert!(family.bitmap.iter().all(|&b| b), "all leaf bits must be set"); } + + // ----------------------------------------------------------------------- + // is_ancestor — partial-order axiom queries on the loaded closure + // ----------------------------------------------------------------------- + + fn build_chain_schema() -> OntologySchema { + // Build a four-tier chain: Heel ← Hip ← Twig ← Leaf. + let src = "\ + @prefix ogit: .\n\ + @prefix rdfs: .\n\ + ogit:Heel a rdfs:Class .\n\ + ogit:Hip a rdfs:Class ; rdfs:subClassOf ogit:Heel .\n\ + ogit:Twig a rdfs:Class ; rdfs:subClassOf ogit:Hip .\n\ + ogit:Leaf a rdfs:Class ; rdfs:subClassOf ogit:Twig ."; + let triples = TurtleParser::parse(src).unwrap(); + OntologySchema::from_triples(&triples).unwrap() + } + + #[test] + fn is_ancestor_reflexive() { + let schema = build_chain_schema(); + assert!(schema.is_ancestor("ogit:Heel", "ogit:Heel")); + assert!(schema.is_ancestor("ogit:Hip", "ogit:Hip")); + assert!(schema.is_ancestor("ogit:Leaf", "ogit:Leaf")); + // Reflexivity must hold even for IRIs that aren't in the schema — + // the relation is conceptually defined on the full IRI space. + assert!(schema.is_ancestor("ogit:Unknown", "ogit:Unknown")); + } + + #[test] + fn is_ancestor_direct_parent() { + let schema = build_chain_schema(); + assert!(schema.is_ancestor("ogit:Heel", "ogit:Hip")); + assert!(schema.is_ancestor("ogit:Hip", "ogit:Twig")); + assert!(schema.is_ancestor("ogit:Twig", "ogit:Leaf")); + } + + #[test] + fn is_ancestor_transitive_through_chain() { + let schema = build_chain_schema(); + // Heel ← Hip ← Twig ← Leaf — transitivity at every depth. + assert!(schema.is_ancestor("ogit:Heel", "ogit:Twig")); + assert!(schema.is_ancestor("ogit:Heel", "ogit:Leaf")); + assert!(schema.is_ancestor("ogit:Hip", "ogit:Leaf")); + } + + #[test] + fn is_ancestor_antisymmetric() { + let schema = build_chain_schema(); + // Reverse direction must NOT hold (except reflexive cases). + assert!(!schema.is_ancestor("ogit:Leaf", "ogit:Heel")); + assert!(!schema.is_ancestor("ogit:Twig", "ogit:Hip")); + assert!(!schema.is_ancestor("ogit:Hip", "ogit:Heel")); + } + + #[test] + fn is_ancestor_unknown_descendant_returns_false() { + let schema = build_chain_schema(); + // Unknown descendant — no chain to walk. Only reflexive case + // returns true (covered by `is_ancestor_reflexive`). + assert!(!schema.is_ancestor("ogit:Heel", "ogit:Unknown")); + assert!(!schema.is_ancestor("ogit:Hip", "ogit:DefinitelyNotInSchema")); + } + + #[test] + fn is_ancestor_unrelated_classes() { + // Two disjoint chains — Heel/Hip and OtherHeel/OtherHip. + let src = "\ + @prefix ogit: .\n\ + @prefix rdfs: .\n\ + ogit:Heel a rdfs:Class .\n\ + ogit:Hip a rdfs:Class ; rdfs:subClassOf ogit:Heel .\n\ + ogit:OtherHeel a rdfs:Class .\n\ + ogit:OtherHip a rdfs:Class ; rdfs:subClassOf ogit:OtherHeel ."; + let triples = TurtleParser::parse(src).unwrap(); + let schema = OntologySchema::from_triples(&triples).unwrap(); + // Across disjoint chains: neither direction holds. + assert!(!schema.is_ancestor("ogit:Heel", "ogit:OtherHip")); + assert!(!schema.is_ancestor("ogit:OtherHeel", "ogit:Hip")); + } } From 730efbd532483faee9e88cc624ebf04861121d33 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 10:39:28 +0000 Subject: [PATCH 2/2] fix(ogit_bridge): reject unknown ancestors in is_ancestor (codex P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex flagged on PR #189 that `is_ancestor` returned true as soon as `entity.parent == ancestor`, even when `ancestor` was not a declared rdfs:Class in `self.entities`. Because `from_triples` accepts `rdfs:subClassOf` targets without requiring them to be declared, a partial or malformed ontology could make `is_ancestor("missing:Class", "ogit:Leaf")` succeed for a non-reflexive unknown ancestor — directly contradicting the method's documented contract that unknown IRIs are only true in the reflexive case. Fix: after the reflexive `ancestor == descendant` shortcut, early-return false if `ancestor` is not present in `self.entities`. Reflexivity on unknown IRIs is preserved (still defined on the full IRI space), but no entity's parent field can now project a phantom IRI into the ancestor closure. Added regression test `is_ancestor_unknown_ancestor_returns_false_when_non_reflexive` that parses a turtle source where `ogit:Leaf rdfs:subClassOf ogit:Phantom` but `ogit:Phantom` is never declared as a class. Asserts both: - `is_ancestor("ogit:Phantom", "ogit:Phantom")` → true (reflexive) - `is_ancestor("ogit:Phantom", "ogit:Leaf")` → false (was previously true — the bug) All 7 `is_ancestor` tests pass; lib clippy + fmt clean. --- src/hpc/ogit_bridge/schema.rs | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/hpc/ogit_bridge/schema.rs b/src/hpc/ogit_bridge/schema.rs index d10e1ae2..945b19b4 100644 --- a/src/hpc/ogit_bridge/schema.rs +++ b/src/hpc/ogit_bridge/schema.rs @@ -630,6 +630,17 @@ impl OntologySchema { return true; } + // Per the documented contract, unknown IRIs are an ancestor ONLY + // in the reflexive case. `from_triples` accepts `rdfs:subClassOf` + // targets without requiring them to be declared classes, so an + // `EntityClass.parent` can legally point at an IRI that's not in + // `self.entities`. Without this guard a malformed/partial schema + // could make `is_ancestor("missing:Class", "ogit:Leaf")` succeed + // (codex P2 on PR #189). + if !self.entities.contains_key(ancestor) { + return false; + } + // Walk the parent chain from descendant upward, looking for ancestor. // Defensive depth cap — see method docstring. const MAX_DEPTH: usize = 64; @@ -905,6 +916,34 @@ mod tests { assert!(!schema.is_ancestor("ogit:Hip", "ogit:DefinitelyNotInSchema")); } + /// Codex P2 regression on PR #189: a class whose `rdfs:subClassOf` + /// edge points at an IRI not declared as a class must NOT satisfy + /// `is_ancestor(unknown_parent, child)`. Only the reflexive case + /// `is_ancestor(unknown, unknown)` should return true. + #[test] + fn is_ancestor_unknown_ancestor_returns_false_when_non_reflexive() { + // ogit:Leaf claims subClassOf ogit:Phantom, but ogit:Phantom is + // never `a rdfs:Class` — TurtleParser/from_triples accepts this + // (rdfs allows undeclared subClassOf targets). + let src = "\ + @prefix ogit: .\n\ + @prefix rdfs: .\n\ + ogit:Leaf a rdfs:Class ; rdfs:subClassOf ogit:Phantom ."; + let triples = TurtleParser::parse(src).unwrap(); + let schema = OntologySchema::from_triples(&triples).unwrap(); + + // The reflexive case still holds — defined on the full IRI space. + assert!(schema.is_ancestor("ogit:Phantom", "ogit:Phantom")); + + // But non-reflexive lookups against the undeclared ancestor MUST + // return false, regardless of any entity's parent field pointing + // at it. + assert!( + !schema.is_ancestor("ogit:Phantom", "ogit:Leaf"), + "is_ancestor must reject ancestors not declared as rdfs:Class" + ); + } + #[test] fn is_ancestor_unrelated_classes() { // Two disjoint chains — Heel/Hip and OtherHeel/OtherHip.