Implement prp-spo2 (owl:propertyChainAxiom), general n-hop, behind an opt-in feature - #59
Open
styk-tv wants to merge 4 commits into
Open
Implement prp-spo2 (owl:propertyChainAxiom), general n-hop, behind an opt-in feature#59styk-tv wants to merge 4 commits into
styk-tv wants to merge 4 commits into
Conversation
OWL 2 RL rule prp-spo2 was not implemented; materialization reported
success and derived nothing for property chain axioms. Adds the rule
behind an opt-in `prp-spo2` Cargo feature, following the structure of the
existing `rdf-12` feature: off by default and strictly additive when off.
cargo test -p reasonable --features prp-spo2
The normative rule is a schema over chain length - LIST[?x, ?p1, ..., ?pn]
with n >= 1 - so it cannot be written as a fixed-arity join. The chain is
encoded structurally and the existing semi-naive fixpoint walks the list:
(base) chain(L,u,v) :- first(L,p), rest(L,nil), T(u,p,v)
(step) chain(L,u,v) :- first(L,p), rest(L,R), T(u,p,w), chain(R,w,v)
(emit) T(u,p,v) :- T(p, owl:propertyChainAxiom, L), chain(L,u,v)
The step rule needs no R != nil guard: rdf:nil carries no rdf:first, so
chain(nil,_,_) is empty and the join contributes nothing. Emitting into
all_triples_input means a hop may be supplied by an inferred triple, so
chains compose with the rest of the rule set as the fixpoint requires.
DisjointSets is deliberately not reused. It unions every list cell into a
single set and collects members by HashMap iteration, so member order is
nondeterministic - immaterial for the set-semantics rules that use it
(cls-int, cls-uni), wrong for chains where p1 o p2 differs from p2 o p1.
A reachability gate keeps intersectionOf/unionOf/oneOf lists out of the
chain relation, so the rule stays cheap on graphs that declare no chains.
Tests are grounded in the W3C OWL 2 test suite. Three published cases
carry test:profile RL and bind a rules engine, and are reproduced in this
file's triple idiom:
New-Feature-ObjectPropertyChain-001 hasMother o hasSister => hasAunt
New-Feature-ObjectPropertyChain-BJP-003 p <- [p,q], a p b, b q c => a p c
New-Feature-ObjectPropertyChain-BJP-004 must NOT entail TransitiveProperty
BJP-003 is the sharp one: the chain head occurs inside its own chain, so
derived triples re-enter the fixpoint. chain2trans1 and BJP-002 are
deliberately excluded - both conclude an axiom rather than an assertion,
which the RL/RDF rule set is knowingly incomplete for.
Every property-chain entailment case in the published corpus uses a
two-element chain, so conformance alone cannot establish generality. An
arity sweep over n in 1..=8, paired with a tightness sweep asserting an
(n-1)-hop path derives nothing, carries the n-hop claim instead.
Also covered: order significance, length-1 equivalence to subPropertyOf,
a hop supplied by inference, shared list suffixes, self-chain closure, the
reachability gate, and cyclic rdf:rest termination.
feature off: 94 passed, 0 failed (unchanged)
feature on: 107 passed, 0 failed
The engine tests would not catch a break in the pyo3 layer, where a working rule can still surface nothing. Adds a `prp-spo2` passthrough feature to the extension crate, forwarding to the engine feature of the same name, mirroring how `abi3` forwards to pyo3. Positive tests skip unless REASONABLE_FEATURE_PRP_SPO2 marks the module under test as built with the rule, so a default wheel collects and skips them at no cost. The W3C BJP-004 non-entailment is deliberately not skipped: it must hold in both configurations, guarding the shipped wheel against over-derivation either way. Cases mirror the engine suite: ObjectPropertyChain-001, an arity sweep over n in 1..=6, order significance, and the non-entailment guard.
Runs the feature's tests as one extra step in the existing job rather than a second job or a matrix leg: it reuses the warm release target and recompiles only `reasonable`, a few seconds, where another job would repeat checkout, toolchain, cross-compiler, apt and uv setup for minutes. The Python extension under test is built by uv through the PEP517 backend, which takes its features from [tool.maturin] in pyproject.toml and not from the `maturin build` step. MATURIN_PEP517_ARGS is the supported way to add the feature there, and --reinstall-package forces the rebuild because uv's cache keys are file-based. Test-only: release wheels are built and published by builds.yml with --features abi3 and are unaffected.
The Python extension forwards the engine feature under the same name, but
the binary had no [features] section at all, so reaching the rule meant
`--features reasonable/prp-spo2` - an awkward asymmetry between the two
surfaces, and easy to miss when installing.
Adds the matching passthrough:
cargo install reasonable-cli --features prp-spo2
Off by default; the default binary is unchanged. Verified against the
fixture from the original report - default build derives 14 triples and
no chain entailment, the feature build derives 15 and includes it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements OWL 2 RL rule
prp-spo2(owl:propertyChainAxiom) for chains of arbitrary length,behind an opt-in Cargo feature. The default build is unchanged.
Closes #23.
Why
The rule was never implemented, so materialization reported success and derived nothing for chain
axioms — a caller could not distinguish fired from ignored without asserting the expected
triple and probing for it. The rule table already recorded this honestly as
no; this changes theanswer rather than the documentation.
Design
The normative rule is a schema over chain length —
LIST[?x, ?p1, ..., ?pn]with n ≥ 1 — so itcannot be written as a fixed-arity join. Rather than unrolling each list into a per-axiom join,
the chain is encoded structurally and the existing semi-naive fixpoint walks it:
Consequences worth noting:
path and no per-length specialisation exists.
R != nilguard.rdf:nilcarries nordf:first, sochain(nil,_,_)is empty and the join contributes nothing.
emitwrites intoall_triples_input, so a hopmay be supplied by an inferred triple (
inverseOf,subPropertyOf, another chain) — which thefixpoint semantics requires and a pre-pass could not see.
chaintuples.Why
DisjointSetsis not reusedThe existing list reader behind
cls-int/cls-uniunions every cell of a list into one disjointset and collects members by iterating a
HashMap, so member order is nondeterministic betweenruns. That is immaterial for intersection and union, which are sets. It is the entire meaning of a
chain:
hasParent ∘ hasBrotheris nothasBrother ∘ hasParent. An implementation routed throughit would be silently and unreproducibly wrong, so the rule reads
rdf:first/rdf:restdirectly.There is a test that fails loudly for that mistake.
Cost when unused
A reachability gate restricts the chain relation to cells an axiom actually references, so
intersectionOf/unionOf/oneOflists never enter it. On graphs that declare no chains therelations stay empty and semi-naive evaluation makes the rule near-free.
Tests
Grounded in the W3C OWL 2 test suite rather than fixtures written to match the implementation.
Three published cases carry
test:profile RLand bind a rules engine:New-Feature-ObjectPropertyChain-001hasMother ∘ hasSister ⊑ hasAunt→Stewie hasAunt CarolNew-Feature-ObjectPropertyChain-BJP-003p ← [p,q],a p b,b q c→a p cNew-Feature-ObjectPropertyChain-BJP-004p a owl:TransitivePropertyBJP-003is the sharp one — the chain head occurs inside its own chain, so derived triplesre-enter the fixpoint.
Two published cases are deliberately excluded:
chain2trans1andBJP-002both conclude anaxiom rather than an assertion, which the RL/RDF rule set is knowingly incomplete for. They are
documented as excluded in the test module so neither is later chased as a bug nor counted as
conformance the rule set does not grant.
On proving n-hop: every property-chain entailment case in the published corpus uses a
two-element chain, so conformance alone cannot establish generality. An arity sweep over n ∈ 1..8,
paired with a tightness sweep asserting an (n−1)-hop path derives nothing, carries that claim
instead. Also covered: order significance, length-1 equivalence to
subPropertyOf, a hop suppliedby inference, shared list suffixes, self-chain closure, the reachability gate, and cyclic
rdf:resttermination.The Python binding is covered too, since the engine tests would not catch a break in the pyo3
layer where a working rule can still surface nothing.
Isolation
The feature follows the structure of the existing
rdf-12feature — same comment form, samestrictly additive when off contract, same
#[cfg(feature = ...)]gating.cargo test --workspace--features prp-spo2The CI addition is one step in the existing job, reusing the warm release target so only
reasonablerecompiles — a few seconds, versus minutes for a second job. The Python extensionunder test is built by uv through the PEP517 backend, which takes features from
[tool.maturin]rather than from thematurin buildstep, soMATURIN_PEP517_ARGSis the onlyway to reach it; release wheels come from
builds.ymland are unaffected.Whether the rule should later become a default feature is your call — the flag is what makes it
safe to land now.