Quickstart
One canon, one question, one computed answer — from a Node application, with nothing but npm. About ten minutes.
The task (0–2 min)
Section titled “The task (0–2 min)”The German Civil Code fixes how a period is counted: when an event starts the period, the day of the event is not counted (section 187), and a period measured in days ends with the last of those days (section 188). The question: an event on 6 March 2026 starts a period of 14 calendar days — does the period end on 20 March?
The canon de.bgb.fristen formalizes those provisions. Your application
supplies the facts and the question; Arxo Law executes the canon and
returns the answer together with its grounds.
You need Node.js 20 or newer and npm. You get a project whose one file asks the question and prints the answer document: the statuses, the rule that fired, and the hashes that reproduce the answer.
Run it (2–5 min)
Section titled “Run it (2–5 min)”npm create arxo@latest my-deadlinecd my-deadlinenpm installnpm startsrc/ask.ts is the whole application. To do the same inside an existing
project, install the facade and the canon next to it — no network is
needed after that:
npm i @arxo/law @arxo/canon-bgb-fristenimport { open } from '@arxo/law';
const fristen = await open('de.bgb.fristen@0.1.0');
const r = await fristen.truth('frist_ende', ['frist', '2026-03-20'], { legalTime: '2026-09-17', timezone: 'Europe/Berlin', deadlinePolicy: 'urn:de:corpus:clir:bgb-fristen#BGB_FRISTEN_TAG', answers: [ { predicate: 'frist_ereignis', args: ['frist', '2026-03-06'] }, { predicate: 'frist_dauer_tage', args: ['frist', '14 calendar_day'] }, ],});
console.log(r.evaluationStatus, r.truthStatus);The names are the canon’s own, in German, and each one has a plain reading:
| Name | Reads as | Role in the call |
|---|---|---|
frist_ereignis |
the event that starts the period | a fact: period frist starts with an event on 2026-03-06 |
frist_dauer_tage |
the length of the period in days | a fact: 14 calendar_day |
frist_ende |
does the period end on this date | the question: frist, 2026-03-20 |
open('de.bgb.fristen@0.1.0') names the canon and its exact version: an
answer without a pinned version is not reproducible. legalTime is the
date the law is projected on, timezone the calendar the dates live in,
and deadlinePolicy names how the days are counted — that policy is
declared by the canon itself, not by your code.
What went in and what came back (5–8 min)
Section titled “What went in and what came back (5–8 min)”The call carries four things: the canon, the facts (answers), the legal
time, and the question (truth — is frist_ende(frist, 2026-03-20)
established). The answer is a document. Printed field by field on this
canon, it reads:
{ "evaluationStatus": "COMPUTED", "truthStatus": "TRUE_ONLY", "sources": [], "rulesApplied": ["urn:de:corpus:clir:bgb-fristen#TagesfristEnde"], "hashes": { "program": "sha256:d1fbe22…", "semantic": "sha256:e679284…", "result": "sha256:e352d71…" }, "via": "local"}| Field | What it says |
|---|---|
evaluationStatus |
whether the answer was computed at all: COMPUTED here; other values say that data is missing, that a court must decide, or that a reading must be chosen |
truthStatus |
what the computation supports: TRUE_ONLY — established, the period ends on 20 March; the other values are FALSE_ONLY, BOTH, NEITHER |
proof |
the proof graph: the facts as assertions and the rule that fired; rulesApplied above is read from it — the rule TagesfristEnde, whose label in the canon cites sections 187 (1) and 188 (1) |
sources |
the source anchors of the applied rules when the canon attaches them to the answer; this canon build returns an empty list, and the rule identifier in proof remains the address |
hashes |
program — the canon that answered; semantic — the case as evaluated; result — the outcome; the same inputs give the same bytes |
issues |
the engine’s messages, by code; on this call there are only defaulted context fields |
document |
the canonical bytes of the whole evaluation document — what you archive or hand to a reviewer |
src/ask.ts of the generated project asks this question and prints the
same fields.
Take a fact away (8–11 min)
Section titled “Take a fact away (8–11 min)”Remove frist_dauer_tage from answers and ask again:
{ "evaluationStatus": "COMPUTED", "truthStatus": "NEITHER" }NEITHER is not “no”. It says that this computation supports neither the
claim nor its negation. To learn why, ask the same question through
whyNot — it returns the blockers: each candidate rule and the premises it
reads:
const w = await fristen.whyNot('frist_ende', ['frist', '2026-03-20'], partial);w.value.value.blockers;[ { "rule": "urn:de:corpus:clir:bgb-fristen#TagesfristEnde", "trigger": "UNDETERMINED", "conjuncts": [ { "literal": { "predicate": "urn:de:corpus:clir:bgb-fristen#frist_ereignis" }, "trigger": "UNDETERMINED" }, { "literal": { "predicate": "urn:de:corpus:clir:bgb-fristen#frist_dauer_tage" }, "trigger": "UNDETERMINED" } ] }, { "rule": "urn:de:corpus:clir:bgb-fristen#Verlegung193", "trigger": "UNDETERMINED", "conjuncts": [ { "literal": { "predicate": "urn:de:corpus:clir:bgb-fristen#frist_ende_kalender" }, "trigger": "UNDETERMINED" } ] }](The literals’ arguments and the engine’s detail strings are omitted here.) Two rules could have
concluded frist_ende. The first needs frist_ereignis and
frist_dauer_tage; the second needs frist_ende_kalender, a date already
computed by another rule. Compare the premises with the facts you supplied
and the gap is named: frist_dauer_tage is what the application must ask
the user for.
const have = new Set(partial.answers.map((f) => f.predicate));const missing = blockers .flatMap((b) => b.conjuncts.map((c) => c.literal.predicate.split('#')[1])) .filter((p) => !have.has(p));// ["frist_dauer_tage", "frist_ende_kalender"]That is how an application asks for more: it does not guess a value and does not fall back to “no”. It shows the missing premise, collects it, and asks again — and the new answer carries its own hashes.
Where the names come from (11–14 min)
Section titled “Where the names come from (11–14 min)”frist_ende, frist_ereignis, frist_dauer_tage were given to you above. In
your own project you will need names nobody has handed you, and there are
four places they live. None of them is a document you keep in sync by hand:
all four are derived from the canon you installed.
The editor. The canon package ships generated declarations
(index.d.ts) and registers them with @arxo/law, so after open(...)
the predicate argument is typed by the canon: your editor completes
frist_ende, and a name the canon does not know fails type checking before
anything runs.
The manifest. questions() returns the canon’s own inventory:
questions you can ask, facts you can supply, their parameters, and the
labels the author wrote. It is what a form or a picker is built from.
const m = fristen.questions('en');const show = (r) => `${r.name}(${r.parameters.map((p) => `${p.name}: ${p.typeShort}`).join(', ')}) — ${r.label}`;show(m.facts.find((f) => f.name === 'frist_ereignis'));show(m.questions.find((q) => q.name === 'frist_ende'));frist_ereignis(f: Frist, tag: Date) — Ereignis oder Zeitpunkt, der für den Anfang der Frist maßgebend ist (§ 187 Abs. 1)frist_ende(f: Frist, tag: Date) — Tag, mit dessen Ablauf die Frist endet (§§ 187, 188, 193)Labels come in the languages the canon’s author wrote them in; this canon
labels its relations in German, so 'en' falls back to that. Each
question also lists relevantFacts: the facts its rules read, which for
frist_ende is where frist_ereignis and frist_dauer_tage appear.
Each parameter carries a type and a widget (entity, date,
quantity, …), so the value you pass as a plain string — '2026-03-06',
'14 calendar_day' — is wrapped by the declared type, not guessed from its
shape.
The passport. passport().inModel('frist_ende') says whether the
question is executable in this version of the canon at all. Build on a
provision the model carries only as text, and every answer will be
NEITHER.
The error. A misspelt name is refused with the nearest real ones:
await fristen.truth('frist_endee', ['frist', '2026-03-20'], caseInput);// FactError: unknown relation "frist_endee"// path: "query", nearest: ["frist_ende", "frist_ende_kalender", "frist_ereignis", ...]And when the name is right but a fact is missing, the previous step
already showed the fifth place: whyNot names the premises the rules were
waiting for.
If you are exploring before writing code, the same inventory is available
to an assistant over MCP: law_search takes a question in your own words
and returns predicate addresses, law_rules lists the facts a rule reads.
That path is Connect an AI assistant.
Where next
Section titled “Where next”- Understand the answer — every field, the three axes your code must
keep apart, and why
NEITHERandFALSE_ONLYdiffer: How to read an answer. - Connect an AI assistant — the same canons over MCP, for an agent or
an editor, with one
curlto the public server: Connect an AI assistant (MCP). - Write your own rules — the language the canon is written in, from a first rule to a package: https://docs.arxo.io/language/.
- Python — the same canon, the same answer:
pip install arxo arxo-canon-bgb-fristenfrom arxo import open
fristen = open("de.bgb.fristen@0.1.0")r = fristen.truth("frist_ende", ["frist", "2026-03-20"], { "legalTime": "2026-09-17", "timezone": "Europe/Berlin", "deadlinePolicy": "urn:de:corpus:clir:bgb-fristen#BGB_FRISTEN_TAG", "answers": [ {"predicate": "frist_ereignis", "args": ["frist", "2026-03-06"]}, {"predicate": "frist_dauer_tage", "args": ["frist", "14 calendar_day"]}, ],})r.evaluationStatus # "COMPUTED"r.truthStatus # "TRUE_ONLY"r.sources; r.proof; r.hashes; r.documentfristen.why_not(...) is the Python name of whyNot.
See it without typing
Section titled “See it without typing”- lens.arxo.io — a published answer with its proof, hashes, and the source provisions.
- zan.arxo.io — the Kazakhstan slice of the corpus: what is formalized and how it answers on worked cases.
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.