@arxo/law
Ask a pinned canon a closed question. Get the answer, its proof and three hashes anyone can replay. A canon is a law, a standard or a rulebook compiled to a package; the engine runs locally in WebAssembly, offline after install, with byte-identical answers on every machine.
Quickstart · How it works · Questions · Facts · Answer · Run it anywhere · Packages · Canons · API
Quickstart
npm i @arxo/[email protected] @arxo/[email protected]
A 14-day period starts with an event on Saturday 7 March 2026. When does it end? (§§ 187, 188, 193 of the German Civil Code)
import { open } from '@arxo/law';
const bgb = await open('[email protected]', { offline: true });
const end = await bgb.collect('frist_ende', ['frist', '?end'], {
legalTime: '2026-09-17', timezone: 'Europe/Berlin',
decisionTime: '2026-09-17T09:00:00Z', knowledgeTime: '2026-09-17T09:00:00Z',
deadlinePolicy: 'urn:de:corpus:clir:bgb-fristen#BGB_FRISTEN_TAG',
answers: [{ predicate: 'frist_ereignis', args: ['frist', '2026-03-07'] },
{ predicate: 'frist_dauer_tage', args: ['frist', '14 calendar_day'] },
],
});
Output — produced by this exact call and checked on every build. Fourteen days from 7 March land on Saturday 21 March, so § 193 moves the end to the next working day:
end.evaluationStatus // 'COMPUTED'
end.value[0].value // '2026-03-23' (Mon, 23 Mar 2026)
end.proof.nodes.length // 4: two facts → rule TagesfristEnde → the question
end.hashes.result // 'sha256:b2faa3a5…' (with program and semantic)
end.document // 4423 canonical bytes — replay them anywhere
Want a ready TypeScript project with this question and a test?
npm create arxo@latest my-app.
What comes back
Every answer carries the proof graph the engine built, the norms it relied on with their pinned text, and the canonical evaluation document. The package itself is transport, case assembly and field projection: there is no second evaluator, and a refusal is a value, not an exception.
How it works
The same mechanics run any canon — a statute, a technical standard, a rulebook or a table of constants. A model can formalize a text; the engine executes it and proves every answer.
Five questions
One ask underneath; pick the shape of the question.
| Method | Asks | Example |
|---|---|---|
truth(p, args, case) | is p(…) established? | does the period end on 23 March? |
collect(p, args, case) | which values? mark the free slot with '?name' | when does the period end? |
deadline({ date, days, unit }, case) | a term under the case's deadline policy | 14 working days from receipt |
calc(term, case) | a computed value | an insurance premium |
positions(case) | duties, prohibitions, powers of the case | who owes what, by when |
Two more read the result rather than ask a new question:
whyNot(p, args, case) returns the blocker graph of a NEITHER, and
unfold(p, { depth }) prints the rules that produce p and their premises,
anchored to the pinned text, with no case at all.
Facts
Facts are { predicate, args, state }. Values are bare; the package's
declarations type them:
| Declared type | Pass |
|---|---|
Date | '2026-03-06' |
Integer, Natural | 14 or '14' |
Decimal | '0.5' — a string, never a JavaScript number |
Money | '730000 KZT' |
Quantity | '14 calendar_day' |
Boolean | true |
| an enum | the variant name, e.g. 'Abschlagszahlung' |
| an entity | a short id ('frist') or a full URN |
Three states, not two: 'yes' (the default for a listed fact), 'no' (an
explicit negative assertion) and 'unknown'. A fact you do not list is
unknown; nothing is defaulted.
An unknown relation, a wrong arity, a wrong type or an unknown enum variant
throws FactError with the path and the nearest names before the engine
runs — which makes the package a safe check layer behind an LLM agent: the
model proposes facts, Arxo evaluates and proves.
Qualified names, dependencies and fully spelled arguments
A relation is named by its short name (root package of the world), by
package::name as in source ('phys.relativity_core::predicted_ratio'), or by
its full StableId. A transitive dependency the link does not list in
imports has no package name in the world: name its symbols by StableId.
An enum of a dependency is 'de.bgb.fristen::Hoechstfristkategorie::Erbfall',
a constant of a dependency 'phys.relativity_core::ComposedSpeedRatio'.
A fully spelled argument ({ kind: 'value', type: { name }, value }) passes
through unchanged.
The answer
| Field | Meaning |
|---|---|
evaluationStatus | the discriminant: COMPUTED, MISSING_INPUT, REQUIRES_JUDGMENT, INTERPRETATION_REQUIRED, … |
truthStatus | a separate axis: TRUE_ONLY, FALSE_ONLY, NEITHER, BOTH |
value | the values of a collect or calc |
missingInputs | what a MISSING_INPUT answer lacks |
whyNot | every candidate rule, its trigger and each conjunct: SATISFIED, NOT_SATISFIED, UNDETERMINED, CONFLICTED |
proof | the proof graph |
sources | the norms behind the answer, with their pinned text |
hashes | { program, semantic, result } |
issues | engine messages verbatim — match the codes, not the text |
document | the canonical bytes of the evaluation document |
via | 'local' or 'mcp'; there is no silent fallback between them |
"Not in the model" and "the law is silent" are different answers:
pkg.passport() repeats the package's formalization measure (EXECUTABLE,
SOURCE_ONLY, …) and pkg.inModel(predicate) says whether a name exists at
all. pkg.explain(answer) returns the host's text over the same proof.
Exceptions are only for the facade's own failures: FactError,
PackageNotFoundError, IntegrityError, TransportError, RpcError.
Run it anywhere
The same methods and the same answer shape on every route.
Node — local engine, offline after install (default)
import { open } from '@arxo/law';
const fristen = await open('[email protected]', { offline: true });
open('name@version') looks for the installed canon, then cacheDir (when
given), then the public registry, and checks every canon against its
contentHash.
The version is required: an answer without a pinned version is not
reproducible.
Browser — no node: imports
import { open } from '@arxo/law/browser'; // or the `browser` export condition
import ir from '@arxo/canon-bgb-fristen/package.lawir.json';
const fristen = await open('[email protected]', {
wasm: fetch('/law_wasm_core.wasm'),
canons: { '[email protected]': { ir, calendar } },
});
Sources are the canons the bundle carries, the origin's Cache Storage (keyed
by contentHash) and the public registry.
Remote host — the Arxo MCP host, no wasm
const picc = await open('[email protected]', { remote: true });
The host answers law_ask; the answer is its projection (evaluationStatus,
truthStatus, whyNot, proof, provenance hashes, raw payload in host),
and document is null. questions() and passport() need the local CLIR.
hostPackage names the catalog key for a remote-only open.
Router — a question in plain words
import { route } from '@arxo/law/router';
const r = await route('Wann endet eine 14-Tage-Frist ab 6. März 2026?', { legalTime: '2026-09-17', answers });
Search → rules → ask on the host. The result is the host's answer or
{ missing } — the facts the chosen norm needs that you did not give — with
the chosen norm and the rejected candidates named.
A case package on disk — saved questions, edits through the law CLI
import { openCasePackage } from '@arxo/law/case-package';
const pkg = await openCasePackage('/path/to/case-package');
const accident = pkg.case('Collision');
accident.questions(); // saved queries and question cards
const answer = await accident.ask('positions');
await accident.proposeFacts('facts.json', { by: 'urn:example:operator', at: '2026-09-17T12:00:00+05:00' });
await accident.acceptFact('p-0001', { by: 'urn:example:operator', at: '2026-09-17T12:00:00+05:00' });
Reads use the same wasm ask as the CLI; edits run the law binary (the
law option, ARXO_LAW_BINARY, the optional peer @arxo/cli, then PATH),
so SDK and CLI cannot diverge. A refused edit throws CaseEditError with
its LPK-E… code.
law serve — a long-running server you host
const law = await open('[email protected]', {
serve: { endpoint: 'http://127.0.0.1:8480', token: process.env.LAW_SERVE_TOKEN },
});
const answer = await law.ask('Prosrochka', { kind: 'positions', queryId: 'positions' });
answer.documentBytes; // the exact embedded evaluation document
law.refreshWorld() picks up a new server generation explicitly. No wasm
and no local CLIR on this route.
.arxo containers — a package or saved answers in one file, opened by URL
import { openContainer } from '@arxo/law/arxo';
const box = await openContainer('https://example.org/ogpo-batch.arxo', { wasm: fetch('/law_wasm.wasm') });
await box.inspect(); // descriptor, containerHash, signature — a few KB of the file
await box.replay('001'); // re-evaluates a saved answer: replay.same, replay.document
The engine reads only the byte ranges it needs (HTTP Range), checks CRC,
sha256 of every file and, with trustedKeys, the signature. Needs the full
module law_wasm.wasm.
Packages
Canons
| Install | Open | Answers |
|---|---|---|
@arxo/canon-bgb-fristen | [email protected] | periods and limitation, §§ 186–213 BGB (Germany) |
@arxo/canon-ogpo-vts | [email protected] | compulsory motor liability insurance (Kazakhstan): premiums, limits, payment terms |
@arxo/canon-unidroit-picc | [email protected] | UNIDROIT Principles 2016: application, good faith, interpretation |
Each @arxo/canon-* package adds its relation names to the CanonRegistry
type, so a misspelled predicate is an editor error. Answer, Fact,
CaseInput and the status unions come from @arxo/law-types.
API
open(spec, options?)
spec is 'name@version'. Returns a package with the methods above.
| Option | Type | Default | |
|---|---|---|---|
offline | boolean | false | never touch the network; explain refuses |
remote | boolean | false | ask the Arxo host instead of the local engine |
serve | { endpoint, token?, fetch? } | — | ask your law serve |
cacheDir | string | — | keep downloaded canons on disk, keyed by contentHash |
registryUrl | string | public registry | where canons are fetched from |
mcpEndpoint | string | Arxo host | the host for remote, explain, route |
hostPackage | string | from the canon | the host's catalog key for a remote-only open |
contentHash | string | — | take exactly these bytes from cacheDir |
wasmPath | string | URL | bundled | a specific engine module |
fetch | typeof fetch | global | your transport |
Case input
| Field | |
|---|---|
legalTime | the date the law is read at — required, never defaulted |
timezone | IANA zone of the case |
deadlinePolicy | how days are counted; without one a term answer is MISSING_POLICY |
answers | the facts |
decisionTime, knowledgeTime | when the decision is made, what was known when |
Errors
| Class | Thrown when |
|---|---|
FactError | a fact does not fit the package: path, nearest |
PackageNotFoundError | no source has name@version |
IntegrityError | canon bytes do not match contentHash |
TransportError, RpcError | the network or the host failed |
CaseEditError | the law CLI refused an edit: LPK-E… code |
Examples
recipes/ holds four
20-line examples, each run in CI: a term end (BGB), a computed value (Kazakh
motor insurance), a condition (UNIDROIT) and the check layer behind an LLM
agent.
Performance
Wasm cold start 77 ms, one truth question 40 ms, law_wasm_core.wasm
4.7 MB (node bench/measure.mjs, 2026-09-21, darwin 24.6, developer laptop).
Links
arxo.io · Changelog ·
@arxo/law-engine — the engine without the facade ·
@arxo/law-types — types ·
Apache-2.0