Skip to content

Commit 6c4607d

Browse files
committed
feat(hpc): widen dn_tree::bundle_into + add OntologySchema::is_ancestor
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 #188 (comment)
1 parent eb6444f commit 6c4607d

2 files changed

Lines changed: 154 additions & 1 deletion

File tree

src/hpc/dn_tree.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,14 @@ fn is_zero(hv: &GraphHV) -> bool {
105105
/// For each bit position, the existing summary bit is kept with probability
106106
/// `1 - lr * boost`, and replaced by the input bit with probability `lr * boost`.
107107
/// This is implemented per-word using a stochastic mask.
108-
fn bundle_into(current: &GraphHV, hv: &GraphHV, lr: f64, boost: f64, rng: &mut SplitMix64) -> GraphHV {
108+
///
109+
/// # Visibility
110+
///
111+
/// Exposed as `pub(crate)` so the Pillar-13 drift-check test in
112+
/// `crate::hpc::pillar::hhtl_contraction` can run the production bundle
113+
/// against its independently-derived Bernoulli-mixture reference. The
114+
/// function is not part of the public API and may change without notice.
115+
pub(crate) fn bundle_into(current: &GraphHV, hv: &GraphHV, lr: f64, boost: f64, rng: &mut SplitMix64) -> GraphHV {
109116
let effective_lr = (lr * boost).min(1.0);
110117
let mut result = current.clone();
111118

src/hpc/ogit_bridge/schema.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,72 @@ impl OntologySchema {
586586
leaf_count,
587587
})
588588
}
589+
590+
/// Returns `true` iff `descendant` is reachable from `ancestor` by
591+
/// walking the `rdfs:subClassOf` chain encoded in
592+
/// [`EntityClass::parent`]. The relation is *reflexive*: every class
593+
/// is its own ancestor (including unknown IRIs — which return `true`
594+
/// only for the reflexive case `ancestor == descendant`).
595+
///
596+
/// # Complexity
597+
///
598+
/// O(depth) — walks at most `MAX_DEPTH = 64` parent links before
599+
/// returning `false`, defensively guarding against any cycle that
600+
/// might have slipped past the partial-order check upstream. Pillar-14
601+
/// asserts the closure is antisymmetric on synthetic DAGs; this
602+
/// method's cycle guard is the runtime backstop for that assertion.
603+
///
604+
/// # Use case
605+
///
606+
/// The Pillar-14 drift-check test in `crate::hpc::pillar::ogit_lattice`
607+
/// uses this method to verify the loaded ontology's closure satisfies
608+
/// the same three partial-order axioms the synthetic-DAG probe
609+
/// certifies. Beyond that, it is the natural query for any
610+
/// type-gated cascade step: "may activation propagate from a tile of
611+
/// type `descendant` to a tile of type `ancestor`?"
612+
///
613+
/// # Example
614+
///
615+
/// ```ignore
616+
/// // Reflexive
617+
/// assert!(schema.is_ancestor("ogit:Heel", "ogit:Heel"));
618+
/// // Transitive via the subClassOf chain
619+
/// assert!(schema.is_ancestor("ogit:Heel", "ogit:SomeLeaf"));
620+
/// // No relation
621+
/// assert!(!schema.is_ancestor("ogit:Leaf", "ogit:Heel"));
622+
/// // Unknown descendant — only reflexive case returns true
623+
/// assert!(!schema.is_ancestor("ogit:Heel", "ogit:Unknown"));
624+
/// ```
625+
pub fn is_ancestor(&self, ancestor: &str, descendant: &str) -> bool {
626+
// Reflexive case — handles both the "X is its own ancestor"
627+
// identity and the unknown-class case (where neither IRI is in
628+
// the schema but they are equal).
629+
if ancestor == descendant {
630+
return true;
631+
}
632+
633+
// Walk the parent chain from descendant upward, looking for ancestor.
634+
// Defensive depth cap — see method docstring.
635+
const MAX_DEPTH: usize = 64;
636+
let mut current: &str = descendant;
637+
for _ in 0..MAX_DEPTH {
638+
let entity = match self.entities.get(current) {
639+
Some(e) => e,
640+
None => return false, // descendant unknown — no chain to walk
641+
};
642+
let parent = match entity.parent.as_deref() {
643+
Some(p) => p,
644+
None => return false, // reached root without finding ancestor
645+
};
646+
if parent == ancestor {
647+
return true;
648+
}
649+
current = parent;
650+
}
651+
// Exceeded depth cap — treat as not-an-ancestor (defensive; this
652+
// path should be unreachable on a well-formed schema).
653+
false
654+
}
589655
}
590656

591657
// ---------------------------------------------------------------------------
@@ -775,4 +841,84 @@ mod tests {
775841
// Both bits must be set
776842
assert!(family.bitmap.iter().all(|&b| b), "all leaf bits must be set");
777843
}
844+
845+
// -----------------------------------------------------------------------
846+
// is_ancestor — partial-order axiom queries on the loaded closure
847+
// -----------------------------------------------------------------------
848+
849+
fn build_chain_schema() -> OntologySchema {
850+
// Build a four-tier chain: Heel ← Hip ← Twig ← Leaf.
851+
let src = "\
852+
@prefix ogit: <http://www.purl.org/ogit/> .\n\
853+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n\
854+
ogit:Heel a rdfs:Class .\n\
855+
ogit:Hip a rdfs:Class ; rdfs:subClassOf ogit:Heel .\n\
856+
ogit:Twig a rdfs:Class ; rdfs:subClassOf ogit:Hip .\n\
857+
ogit:Leaf a rdfs:Class ; rdfs:subClassOf ogit:Twig .";
858+
let triples = TurtleParser::parse(src).unwrap();
859+
OntologySchema::from_triples(&triples).unwrap()
860+
}
861+
862+
#[test]
863+
fn is_ancestor_reflexive() {
864+
let schema = build_chain_schema();
865+
assert!(schema.is_ancestor("ogit:Heel", "ogit:Heel"));
866+
assert!(schema.is_ancestor("ogit:Hip", "ogit:Hip"));
867+
assert!(schema.is_ancestor("ogit:Leaf", "ogit:Leaf"));
868+
// Reflexivity must hold even for IRIs that aren't in the schema —
869+
// the relation is conceptually defined on the full IRI space.
870+
assert!(schema.is_ancestor("ogit:Unknown", "ogit:Unknown"));
871+
}
872+
873+
#[test]
874+
fn is_ancestor_direct_parent() {
875+
let schema = build_chain_schema();
876+
assert!(schema.is_ancestor("ogit:Heel", "ogit:Hip"));
877+
assert!(schema.is_ancestor("ogit:Hip", "ogit:Twig"));
878+
assert!(schema.is_ancestor("ogit:Twig", "ogit:Leaf"));
879+
}
880+
881+
#[test]
882+
fn is_ancestor_transitive_through_chain() {
883+
let schema = build_chain_schema();
884+
// Heel ← Hip ← Twig ← Leaf — transitivity at every depth.
885+
assert!(schema.is_ancestor("ogit:Heel", "ogit:Twig"));
886+
assert!(schema.is_ancestor("ogit:Heel", "ogit:Leaf"));
887+
assert!(schema.is_ancestor("ogit:Hip", "ogit:Leaf"));
888+
}
889+
890+
#[test]
891+
fn is_ancestor_antisymmetric() {
892+
let schema = build_chain_schema();
893+
// Reverse direction must NOT hold (except reflexive cases).
894+
assert!(!schema.is_ancestor("ogit:Leaf", "ogit:Heel"));
895+
assert!(!schema.is_ancestor("ogit:Twig", "ogit:Hip"));
896+
assert!(!schema.is_ancestor("ogit:Hip", "ogit:Heel"));
897+
}
898+
899+
#[test]
900+
fn is_ancestor_unknown_descendant_returns_false() {
901+
let schema = build_chain_schema();
902+
// Unknown descendant — no chain to walk. Only reflexive case
903+
// returns true (covered by `is_ancestor_reflexive`).
904+
assert!(!schema.is_ancestor("ogit:Heel", "ogit:Unknown"));
905+
assert!(!schema.is_ancestor("ogit:Hip", "ogit:DefinitelyNotInSchema"));
906+
}
907+
908+
#[test]
909+
fn is_ancestor_unrelated_classes() {
910+
// Two disjoint chains — Heel/Hip and OtherHeel/OtherHip.
911+
let src = "\
912+
@prefix ogit: <http://www.purl.org/ogit/> .\n\
913+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n\
914+
ogit:Heel a rdfs:Class .\n\
915+
ogit:Hip a rdfs:Class ; rdfs:subClassOf ogit:Heel .\n\
916+
ogit:OtherHeel a rdfs:Class .\n\
917+
ogit:OtherHip a rdfs:Class ; rdfs:subClassOf ogit:OtherHeel .";
918+
let triples = TurtleParser::parse(src).unwrap();
919+
let schema = OntologySchema::from_triples(&triples).unwrap();
920+
// Across disjoint chains: neither direction holds.
921+
assert!(!schema.is_ancestor("ogit:Heel", "ogit:OtherHip"));
922+
assert!(!schema.is_ancestor("ogit:OtherHeel", "ogit:Hip"));
923+
}
778924
}

0 commit comments

Comments
 (0)