Every post so far reads a graph, queries it, or checks it against a
schema or a shape. RIF (Rule Interchange Format) does something
different: a RIF document is itself data — <Forall>/<Frame>/
<Implies> rules written in RIF-XML — and evaluating it means
forward-chaining those rules against a fact base until nothing new
follows. Where RDFS/OWL 2 RL closure (post 3) hard-codes a fixed rule
set inside the engine, RIF lets a document author its own rules.
RIF Core is a full production-rule language. This project implements
the fragment its vendored W3C test cases actually exercise:
Forall/Frame/And/Implies rule bodies translated to SPARQL
basic graph patterns, forward-chaining fixpoint saturation, and
single-<Import> companion-graph resolution — see
docs/claude-rules/scope.md's
RIF Core section for the exact boundary, including what's explicitly
not implemented (RIF list terms, the full date/time/duration
builtin family, RIF-PRD production actions). That honesty matters here
more than most posts: RIF is a big spec, and this is a real subset of
it, not the whole thing.
Measured against the full W3C RIF Core distribution (the original 4
vendored SPARQL-manifest cases plus the 46-test Core dialect corpus,
50 tests total): 46 pass, 1 labelled fail, 3 precise skips (of
50) — see
the test-results dashboard for the
current run and
bin/rif-runner/README.md
for the per-test disposition table (the 1 fail is a data defect in
the official corpus itself, confirmed against the archived W3C wiki
source; each of the 3 skips names its specific unimplemented builtin
or construct — no blanket "unsupported" lines). The subset in scope
for the rest of this post — frame/BGP rule bodies over ground facts —
is squarely inside the 46 that pass.
RIF's Frame construct is the same idea as an RDF triple, written as
"object has slot: value" instead of subject-predicate-object. This
rule (mined directly from the W3C RIF Core test suite's Frames test
case, vendored at
third_party/testing/rif/tc/Frames/)
says: if a customer's status is "gold", that customer gets a 10%
discount:
<Forall>
<declare><Var>Customer</Var></declare>
<formula>
<Implies>
<if>
<Frame>
<object><Var>Customer</Var></object>
<slot ordered="yes">
<Const type="&rif;iri">http://example.org/example#status</Const>
<Const type="&xs;string">gold</Const>
</slot>
</Frame>
</if>
<then>
<Frame>
<object><Var>Customer</Var></object>
<slot ordered="yes">
<Const type="&rif;iri">http://example.org/example#discount</Const>
<Const type="&xs;integer">10</Const>
</slot>
</Frame>
</then>
</Implies>
</formula>
</Forall>
The real fixture pairs this with a second rule for silver status
(discount 5) and one ground fact (customer017 is gold). Below,
both rules run live against two customers — one gold, one silver —
via Factoidal.rifEval(rifRulesXml, dataNQuads)
(npm/factoidal/browser.js's raw RIF export, forward-chaining
saturation over RIF.Core.Eval.fst's fixpoint). The call is wrapped
in a try/catch: an older engine bundle that predates the RIF export
would throw "the loaded factoidal-npm-entry bundle predates the RIF
exports" rather than silently produce nothing, so this cell reports
that instead of failing invisibly.
const rifRulesXml = `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="http://www.w3.org/2007/rif#">
<payload>
<Group>
<sentence>
<Forall>
<declare><Var>Customer</Var></declare>
<formula>
<Implies>
<if>
<Frame>
<object><Var>Customer</Var></object>
<slot ordered="yes">
<Const type="http://www.w3.org/2007/rif#iri">http://example.org/example#status</Const>
<Const type="http://www.w3.org/2001/XMLSchema#string">gold</Const>
</slot>
</Frame>
</if>
<then>
<Frame>
<object><Var>Customer</Var></object>
<slot ordered="yes">
<Const type="http://www.w3.org/2007/rif#iri">http://example.org/example#discount</Const>
<Const type="http://www.w3.org/2001/XMLSchema#integer">10</Const>
</slot>
</Frame>
</then>
</Implies>
</formula>
</Forall>
</sentence>
<sentence>
<Forall>
<declare><Var>Customer</Var></declare>
<formula>
<Implies>
<if>
<Frame>
<object><Var>Customer</Var></object>
<slot ordered="yes">
<Const type="http://www.w3.org/2007/rif#iri">http://example.org/example#status</Const>
<Const type="http://www.w3.org/2001/XMLSchema#string">silver</Const>
</slot>
</Frame>
</if>
<then>
<Frame>
<object><Var>Customer</Var></object>
<slot ordered="yes">
<Const type="http://www.w3.org/2007/rif#iri">http://example.org/example#discount</Const>
<Const type="http://www.w3.org/2001/XMLSchema#integer">5</Const>
</slot>
</Frame>
</then>
</Implies>
</formula>
</Forall>
</sentence>
</Group>
</payload>
</Document>`;
const dataNQuads =
'<http://example.org/example#customer017> <http://example.org/example#status> "gold" .\n' +
'<http://example.org/example#customer017> <http://example.org/example#name> "John Doe" .\n' +
'<http://example.org/example#customer042> <http://example.org/example#status> "silver" .\n' +
'<http://example.org/example#customer042> <http://example.org/example#name> "Jane Roe" .\n';
try {
const result = await Factoidal.rifEval(rifRulesXml, dataNQuads);
const discountLines = result.saturatedNquads
.split("\n")
.filter((line) => line.includes("#discount"));
return {
available: true,
inputCount: result.inputCount,
derivedCount: result.derivedCount,
discounts: discountLines,
};
} catch (err) {
return { available: false, note: err.message };
}
Four input facts (two customers, each with a status and a name), two
derived facts — customer017 gets the gold rule's 10, customer042
gets the silver rule's 5. Neither discount triple was asserted
anywhere in the data; both came purely from forward-chaining the two
rules to a fixpoint.
The rules only fire when their if frame actually matches. A third
customer with a status the rules don't mention (bronze) should
derive nothing:
const rifRulesXml = `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="http://www.w3.org/2007/rif#">
<payload>
<Group>
<sentence>
<Forall>
<declare><Var>Customer</Var></declare>
<formula>
<Implies>
<if>
<Frame>
<object><Var>Customer</Var></object>
<slot ordered="yes">
<Const type="http://www.w3.org/2007/rif#iri">http://example.org/example#status</Const>
<Const type="http://www.w3.org/2001/XMLSchema#string">gold</Const>
</slot>
</Frame>
</if>
<then>
<Frame>
<object><Var>Customer</Var></object>
<slot ordered="yes">
<Const type="http://www.w3.org/2007/rif#iri">http://example.org/example#discount</Const>
<Const type="http://www.w3.org/2001/XMLSchema#integer">10</Const>
</slot>
</Frame>
</then>
</Implies>
</formula>
</Forall>
</sentence>
</Group>
</payload>
</Document>`;
const dataNQuads =
'<http://example.org/example#customer099> <http://example.org/example#status> "bronze" .\n' +
'<http://example.org/example#customer099> <http://example.org/example#name> "Sam Bronze" .\n';
try {
const result = await Factoidal.rifEval(rifRulesXml, dataNQuads);
return {
available: true,
inputCount: result.inputCount,
derivedCount: result.derivedCount,
rounds: result.rounds,
};
} catch (err) {
return { available: false, note: err.message };
}
Two input facts, zero derived — derivedCount: 0. The rule engine
doesn't guess; a frame rule that doesn't match produces nothing,
exactly the way a SPARQL query with no matching bindings returns an
empty result set rather than a fabricated one.
The next post leaves rules behind and goes back to plain syntax — parsing the same graph from five different concrete RDF syntaxes and showing the bytes converge.
Every live cell above is pinned in
tests/hub/post10_test.mjs,
executed against the real npm-entry ABI the same way the in-browser
Factoidal binding is, rather than a hand-copied approximation.