Tiramemsu

Reasoning with N3 · Chapter 4 of 10

Chapter 4

Time, part 1: transaction time

Every reasoning run happens on a view. On an as-of view it reasons over memory as it was believed then, with the rules believed then, and with a clock that reads then. So an answer about the past never changes.

Every run has a view

reason is a method of View, the same object that runs SPARQL and Cypher. Whatever the view can see is what the run can load. The now view sees what is believed now. as_of(TimeRef::Tx(n)) sees what was believed after transaction n. as_of(TimeRef::Instant(ms)) does the same for a wall-clock instant.

// Transaction 2 asserted (v:socrates rdf:type v:Human).
let rules = Rules::Text(r#"@prefix v: <urn:tiramemsu:v:>.
{ ?x a v:Human } => { ?x a v:Mortal }."#.into());

let then = db.as_of(TimeRef::Tx(1)).reason(rules.clone(), &ReasonOptions::default())?;
assert!(then.conclusions.is_empty());          // nothing was known yet

let now = db.now().reason(rules, &ReasonOptions::default())?;
assert_eq!(now.conclusions.len(), 1);          // (v:socrates rdf:type v:Mortal)

The command line takes the same view flags as tm query:

tm reason memory.db --rules mortality.n3 --as-of 1    # no conclusion
tm reason memory.db --rules mortality.n3              # (v:socrates rdf:type v:Mortal)

Tiramemsu guarantees that an as_of(t) view never changes once transaction t has committed. Reasoning inherits the guarantee: the same rules on the same as-of view give the same conclusions, today and in a year.

Rules as they were

A rule set stored with store_rules is a statement (chapter 6). Editing it is a supersede, so the old version is still visible in views of the past. Rules::Stored(rule_set) reads the version that is live in the reasoning view:

Reasoning as of transaction 3 Three lifetimes on the transaction-time axis. The fact that Socrates is human is believed from transaction 2 on. Rule set version A is believed from transaction 2 until transaction 5, when version B supersedes it. A line at transaction 3 crosses the fact and version A, so a run as of transaction 3 uses both. A line at now crosses the fact and version B. e1 · socrates a Human R · mortality, version A R′ · mortality, version B as_of(Tx(3)) now 1 2 3 4 5 6 transaction time →
Rules are statements, so they have lifetimes like facts. A run as of transaction 3 sees e1 and version A. A run on the now view sees e1 and version B.
// Version A stored in transaction 2, version B in transaction 5.
let r = db.as_of(TimeRef::Tx(3)).reason(Rules::Stored(v("mortality")), &ReasonOptions::default())?;
// r ran version A: the rules the agent had at transaction 3.

This answers a question audits ask and few systems can: what would our rules have concluded then, from what we knew then? Not today’s rules over yesterday’s data, and not yesterday’s rules over today’s data. Both clocks are wound back together.

The pinned clock

A rule may ask for the time. In plain N3 that is time:localTime, which reads the system clock. On an as-of view that would be wrong twice over: the run would reason about the present while looking at the past, and two runs would disagree.

Tiramemsu pins the clock to the view:

clock.n3

@prefix v: <urn:tiramemsu:v:>.
@prefix time: <http://www.w3.org/2000/10/swap/time#>.

{ "" time:localTime ?t } => { v:run v:at ?t }.

Run this twice on as_of(TimeRef::Tx(3)) and both runs conclude the same ?t: the instant of transaction 3.

A more useful example: which tasks were overdue at the moment the agent made a decision?

overdue.n3

@prefix v: <urn:tiramemsu:v:>.
@prefix tm: <urn:tiramemsu:tm:>.
@prefix math: <http://www.w3.org/2000/10/swap/math#>.

{ ?task v:due ?due.
  tm:run tm:now ?now.
  ?now math:greaterThan ?due
} => { ?task a v:Overdue }.
let decided_at = TimeRef::Instant(ms("2026-10-20T09:00:00Z")); // ms(): your date-to-epoch-ms helper
let r = db.as_of(decided_at).reason(Rules::Stored(v("overdue")), &ReasonOptions::default())?;
// Overdue as of that morning, by what the agent knew that morning.

A rule set that reads the clock is clock-dependent. reason runs it. materialize refuses it, because a stored “overdue” would be true when written and false a week later, with no premise retracted to say so. Chapter 6 has the full classification.

Two clocks, never mixed

tm:now is transaction time: when the database is looking from. The next chapter brings valid time, when facts held in the world, with its own virtual triples tm:segmentFrom and tm:segmentTo. They are different axes. A run as of last Tuesday can still reason about 2019. tm:now stays last Tuesday, and the segments are in 2019.