Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/hpc/dn_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
185 changes: 185 additions & 0 deletions src/hpc/ogit_bridge/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,83 @@ 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;
}

// 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;
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;
Comment on lines +657 to +658
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unknown ancestors in ancestry checks

is_ancestor returns true as soon as entity.parent == ancestor, even if that parent IRI is not present in self.entities. Because from_triples stores rdfs:subClassOf targets without requiring them to be declared classes, a partial/malformed ontology can make is_ancestor("missing:Class", "ogit:Leaf") succeed for a non-reflexive unknown ancestor. This contradicts the method’s documented contract (“unknown IRIs” are only true in the reflexive case) and can produce false positives in drift checks or type-gated propagation when schemas are incomplete.

Useful? React with 👍 / 👎.

}
current = parent;
}
// Exceeded depth cap — treat as not-an-ancestor (defensive; this
// path should be unreachable on a well-formed schema).
false
}
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -775,4 +852,112 @@ 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: <http://www.purl.org/ogit/> .\n\
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\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"));
}

/// 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: <http://www.purl.org/ogit/> .\n\
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\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.
let src = "\
@prefix ogit: <http://www.purl.org/ogit/> .\n\
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\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"));
}
}
Loading