Reasoning with N3 · Chapter 8 of 10
Chapter 8
Recipes
Seven worked problems. Each one shows the rules, the call, what comes back and the catch.
1. Tenure from validity · 2. Colleagues who overlapped · 3. Trusted facts · 4. Layered rule sets · 5. Contradiction fuses · 6. Previewing a rule edit · 7. Explaining a past conclusion
1. Tenure longer than two years
Problem. Mark people who stayed at an employer for more than two years. The employment facts already carry valid time. Nobody should have to copy dates into extra properties.
v:tenure
@prefix v: <urn:tiramemsu:v:>.
@prefix tm: <urn:tiramemsu:tm:>.
@prefix math: <http://www.w3.org/2000/10/swap/math#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
# Closed employments: the interval is in the statement.
{ ?e tm:content { ?p v:worksAt ?c };
tm:validFrom ?from;
tm:validTo ?to.
(?to ?from) math:difference ?tenure.
?tenure math:greaterThan "P730D"^^xsd:duration # 2 × 365 days
} => { ?p v:longTenureAt ?c }.
For e1 = (v:alice v:worksAt v:acme) valid [2020-01-01, 2023-01-01), the rule concludes (v:alice v:longTenureAt v:acme). Under pointwise semantics the conclusion holds while its premise does, so its interval is e1’s, [2020, 2023). That reads as “during this employment, Alice was a long-tenured employee”. If you want a timeless statement instead, run with TimeMode::Atemporal and the conclusion is unbounded.
The rule set is monotonic: tm:validFrom and tm:validTo are data, not a clock. It can be materialized, and a correction to e1’s dates retracts the conclusion.
Open employments have no tm:validTo, so the rule above skips them. Their tenure depends on today, which makes it a question for reason, asked on a snapshot of the present:
v:tenure-today
@prefix v: <urn:tiramemsu:v:>.
@prefix tm: <urn:tiramemsu:tm:>.
@prefix math: <http://www.w3.org/2000/10/swap/math#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
{ ?e tm:content { ?p v:worksAt ?c };
tm:validFrom ?from.
tm:run tm:now ?now.
(?now ?from) math:difference ?tenure.
?tenure math:greaterThan "P730D"^^xsd:duration
} => { ?p v:longTenureAt ?c }.
let today = db.now().valid_at(ms("2026-10-28")); // ms(): your date-to-epoch-ms helper
let r = today.reason(Rules::Stored(v("tenure-today")), &ReasonOptions::default())?;
Catch. v:tenure-today is ClockDependent, so materialize refuses it. That is the point: a stored “long tenure” computed from today would silently become wrong. The difference of two xsd:dateTime values is an xsd:duration such as "PT94694400S", and eyeron compares it with another xsd:duration (or with a number of seconds).
2. Colleagues who overlapped in time
Problem. Two people are colleagues while both work at the same company. Interval arithmetic in rules is tedious and easy to get wrong.
v:colleagues
@prefix v: <urn:tiramemsu:v:>.
@prefix log: <http://www.w3.org/2000/10/swap/log#>.
{ ?a v:worksAt ?c. ?b v:worksAt ?c. ?a log:notEqualTo ?b } => { ?a v:colleagueOf ?b }.
The rule says nothing about time, and it does not need to. With these facts:
- Alice at Acme
[2019, 2023) - Bob at Acme
[2021, 2025) - Carol at Acme from
2024
the sweep gives exactly the overlaps: Alice and Bob are colleagues [2021, 2023), Bob and Carol [2024, 2025), in both directions. Alice and Carol never are. Each conclusion rests on the two employment statements and the rule set. If Bob’s start date is corrected, his colleague conclusions go with the old fact, and the next run writes them with the new overlap.
Catch. Every employment start and end is a cut. A company with thousands of staff changes over decades makes many segments. Scope the run to a company with ReasonScope::Seeds, or use a window.
3. Trusted facts from confidence layers
Problem. Downstream rules and queries should only use employment facts the agent is confident about. Confidence is a layer on the fact.
v:trust
@prefix v: <urn:tiramemsu:v:>.
@prefix tm: <urn:tiramemsu:tm:>.
@prefix math: <http://www.w3.org/2000/10/swap/math#>.
{ ?e v:confidence ?c.
?c math:greaterThan 0.8.
?e tm:content { ?s v:worksAt ?o }
} => { ?s v:trustedWorksAt ?o }.
With e1 = (v:alice v:worksAt v:acme) carrying the layer (e1 v:confidence 0.9), and (v:bob v:worksAt v:globex) carrying 0.6, only Alice’s fact becomes (v:alice v:trustedWorksAt v:acme). Its premises are the layer and e1. Materialize it, then lower Alice’s confidence to 0.5 with supersede: the old layer is a support, so the trusted fact is retracted in that transaction, and the next run does not derive it again.
Catch. Name the predicate inside tm:content. { ?s ?p ?o } there would turn pruning off and load the whole scope (chapter 3). Write one rule per predicate you trust.
4. Layered rule sets
Problem. Some conclusions build on others. Keep each step in its own rule set, so an edit to one step retracts only what depends on it.
v:where-people-work
@prefix v: <urn:tiramemsu:v:>.
{ ?p v:worksAt ?c. ?c v:locatedIn ?l } => { ?p v:worksIn ?l }.
v:countries
@prefix v: <urn:tiramemsu:v:>.
{ ?p v:worksIn ?city. ?city v:inCountry ?k } => { ?p v:worksInCountry ?k }.
db.materialize(v("where-people-work"), &MaterializeOptions::default())?;
db.materialize(v("countries"), &MaterializeOptions::default())?;
Each v:worksInCountry conclusion rests on a v:worksIn conclusion, a v:inCountry fact and the v:countries rule set. Retract an employment and both levels fall. Edit v:countries and only the upper level is retracted. Edit v:where-people-work and both levels are retracted, because the upper conclusions rest on the lower ones.
Catch. Order matters. Materialize from the bottom up. A rule set reads the conclusions of others as they are when it runs, and does not run them.
5. Contradiction detection with a fuse
Problem. Nobody can be an employee and a contractor of the same company at the same time. Find violations, and keep inconsistent data from producing conclusions.
v:contracts
@prefix v: <urn:tiramemsu:v:>.
{ ?x v:employeeOf ?c. ?x v:contractorOf ?c } => false.
Because of pointwise semantics, the fuse fires only for overlapping intervals. Bob, a contractor of Acme [2019, 2021) and an employee from 2021, is fine. Carol, listed as both during 2024, is not. reason reports the contradiction with the rule and the instance:
let r = db.now().reason(Rules::Stored(v("contracts")), &ReasonOptions::default())?;
if let Some(x) = &r.contradiction {
println!("rule {} fired for {:?}", x.rule, x.instance); // carol, acme
}
Put the fuse in the same rule set as the rules that derive from these facts, and materialize becomes a consistency gate. A fired fuse fails it with N3Contradiction before anything is written:
match db.materialize(v("staffing"), &MaterializeOptions::default()) {
Err(Error::N3Contradiction { rule, .. }) => println!("inconsistent data, rule {rule}; nothing written"),
other => { other?; }
}
Catch. A fuse sees only what the run loads. Scoped or pruned runs check only their part of memory.
6. Previewing a rule edit
Problem. Before you change a rule set, see what the change will retract and what the new version will conclude.
Everything the current version produced rests on its statement R, so dependents(R) lists it: every conclusion, its links and labels, and the conclusions of other rule sets built on top. Find R with a query, or keep the eid store_rules returned.
PREFIX n3: <urn:tiramemsu:n3:>
SELECT ?r WHERE { v:where-people-work n3:source ?text ~ ?r }
// What goes: everything that rests on the current version.
let at_stake = db.now().dependents(r)?;
println!("{} statements rest on the current rules", at_stake.len());
// The same, as the exact report of the edit, without committing it.
let dry = db.transact(TxOptions { dry_run: true, ..TxOptions::default() }, |tx| {
tx.store_rules(v("where-people-work"), NEW_TEXT)?;
Ok(())
})?;
println!("{:?}", dry.retracted); // old conclusions, kind Supersede
// What comes: the new version's conclusions, read-only.
let next = db.now().reason(Rules::Text(NEW_TEXT.into()), &ReasonOptions::default())?;
Catch. dependents is never truncated. On a large rule set the list can be long. Compare its length with your own limit before acting on it.
7. Explaining a past conclusion
Problem. An agent acted on a conclusion last month. It has since been retracted. What did it rest on?
Nothing was deleted. The conclusion, its support links and its labels are all in history. On an as_of view from before the retraction they are live, and the same query that explains a current conclusion explains the old one:
PREFIX n3: <urn:tiramemsu:n3:>
SELECT ?rules ?premise WHERE {
SERVICE <urn:tiramemsu:tm:asOf/150> {
v:alice v:worksIn v:berlin ~ ?d .
?d n3:derivedBy ?rules ;
sys:derivedFrom ?premise .
}
}
The premises include the rule-set statement of the version that was live at transaction 150. The history query of chapter 6 gives its text.