Python
The facade is arxo at version 0.2.0 with the canon package
arxo-canon-bgb-fristen at 0.1.1, which carries the canon
de.bgb.fristen at 0.1.0. You need Python 3.12 or newer. Execution is
local through the bundled WebAssembly engine; this release has no
serve mode, no registry fetch, and no remote execution, so the only
call that reaches a host is explain — and a package opened
offline refuses even that.
Install
Section titled “Install”Two routes, different promises:
# Reproduce the pinned example: the versions in the compatibility record.pip install arxo==0.2.0 arxo-canon-bgb-fristen==0.1.1# Install current versions: newest compatible pair, not the pinned one.pip install arxo arxo-canon-bgb-fristenThe first route installs the pair listed on the compatibility page. The second may install newer releases; re-run the probe on the compatibility page when a version changes.
A first call
Section titled “A first call”The frist scenario from the section map, checked with truth:
from 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 # [] on this canon buildr.hashes # {"program": ..., "semantic": ..., "result": ...}Remove frist_dauer_tage and the answer is COMPUTED with NEITHER;
why_not over the partial case returns two blockers, one per
candidate rule, each with the premises it was waiting for.
The rest of the page is the detail behind this call: open
and how it resolves a spec, the LawPackage methods,
the Answer fields, the errors, and the
operations Python does not ship.
def open(spec: str, options: dict[str, Any] | None = None) -> LawPackage:from arxo import open
fristen = open("de.bgb.fristen@0.1.0")The spec always carries a version; a versionless spec is refused before anything is resolved:
def parse_spec(spec: str | None) -> dict[str, str]:parse_spec("de.bgb.fristen@0.1.0") # {"name": "de.bgb.fristen", "version": "0.1.0"}parse_spec("de.bgb.fristen") # PackageNotFoundError: open() requires name@versionResolution order is an installed canon package, then a checkout tree
passed as local, then the disk cache. When none of the three
sources has the package, open raises PackageNotFoundError
(“registry fetch is not in this release”). The loading chapter
details each source and the hash check.
There is no serve mode in this release: open always returns a
LawPackage with a locally loaded engine. (TypeScript has serve
and router paths that send questions over the network; Python
does not ship them.)
LawPackage
Section titled “LawPackage”Methods are wrappers over ask; they do not evaluate twice. The
snake_case names are canonical; whyNot is kept as an alias for the
JavaScript name.
class LawPackage: def passport(self) -> Passport: ... def explain(self, answer: Any, node_id: str | None = None) -> dict[str, Any]: ... def ask(self, request: dict[str, Any]) -> Answer: ... def truth(self, predicate: str, args: list[Any], case_input: dict[str, Any]) -> Answer: ... def why_not(self, predicate: str, args: list[Any], case_input: dict[str, Any]) -> Answer: ... def whyNot(self, predicate: str, args: list[Any], case_input: dict[str, Any]) -> Answer: ... def collect(self, predicate: str, args: Any, case_input: dict[str, Any]) -> Answer: ... def deadline(self, op_args: dict[str, Any], case_input: dict[str, Any]) -> Answer: ... def calc(self, term: Any, case_input: dict[str, Any]) -> Answer: ... def positions(self, case_input: dict[str, Any]) -> Answer: ...ask takes a request dict with kind and caseInput:
fristen.ask({ "kind": "truth", "predicate": "frist_ende", "args": ["frist", "2026-03-20"], "caseInput": case_input,})Supported kinds are truth, why_not, collect, calendar_op,
term, and positions; anything else raises TypeError. collect
names the free position with ?name in the argument list, or takes an
explicit free (and optionally a comprehension) when args is a
dict. deadline sends {"op": "deadline", **op_args} as a
calendar_op; calc sends the term as a term.
Answers
Section titled “Answers”@dataclassclass Answer: evaluationStatus: str sources: Any proof: Any hashes: dict[str, str] issues: Any document: bytes via: Literal["local", "mcp"] truthStatus: Any = None value: Any = None # Blocker graph of a `why_not` answer; None on other kinds. whyNot: Any = None whyNotTermErrors: Any = None missingInputs: Any = None positions: Any = Nonedocument is the canonical bytes of the evaluation document — what
you archive or hand to a reviewer. hashes carries program,
semantic, and result. The blocker graph of a why_not answer is
read from the value map into whyNot; on other kinds it is None.
Passport and explain
Section titled “Passport and explain”fristen.passport().in_model("frist_ende") # Truefristen.passport().inModel("frist_ende") # True, JavaScript namein_model says whether the relation exists in the shipped world at
all — “not in the model” is a different answer from “the law is
silent”. explain calls the host’s law_explain over the document
the answer carries and returns its text with via: "mcp". It needs
the host: an offline package raises TransportError, and an answer
without a proof graph is refused.
Errors
Section titled “Errors”class LawClientError(Exception): ... # common ancestor of transport failuresclass IntegrityError(LawClientError): ...class ProtocolError(LawClientError): ...class RpcError(LawClientError): ...class TransportError(LawClientError): ...class ValidationError(LawClientError): ...class FactError(Exception): ... # path, nearestclass PackageNotFoundError(Exception): ... # package_name, versionFour different situations, three different treatments:
- Absent fact: a well-formed fact simply not sent. No
exception: the answer is a value,
COMPUTEDwithNEITHER(removingfrist_dauer_tagefrom the first call shows it). - Malformed fact: wrong shape or arity
(
frist_ereignis: expected 2 argument(s), got 0), an object without kind, an unsupported value, a non-object case, a missinglegalTime. RaisesFactErrorwith the failing path. - Unknown relation: a predicate the canon never declared
(
unknown relation "no_such_relation"). RaisesFactError. - Missing package: no source has the pinned canon. Raises
PackageNotFoundErrornaming the package and version.
A fifth case belongs to the application, not the SDK: a required form field the user left empty. The example refuses that with its own validation error before any fact is built — the engine never sees it. The results chapter tables every class with the condition that raises it.
What Python does not ship
Section titled “What Python does not ship”Python has no questions, no unfold, no focused_truth, no
verbalize, no serve mode, no router, and no registry fetch. The
workaround for the missing manifest is to read the inventory at author
time from the TypeScript side — questions() on the same pinned canon
returns the facts, questions, parameters, and labels a form or a
picker is built from — or to read the canon’s shipped index directly.
Author-time means the manifest never travels with the Python answer;
it is read once while the form is written, and the running code sends
only facts the manifest already described.
Execution and network notes
Section titled “Execution and network notes”- The evaluator is the bundled
law_wasm_core.wasm, hosted throughwasmtime; passwasmPathin the options to load another build. - The calendar comes from the installed canon when it ships one, else
from the
calendaroption. - A case already lowered (assertions with
kind: 'assertion') is passed through; otherwise the facade assembles the case fromanswersand wraps each bare value by its declared type. - The only network call in this release is
explainthrough the MCP client made on first use — unless the package was opened offline, which raisesTransportErrorinstead. Queries (truth,why_not,collect,deadline,calc,positions) never leave the process: there is no serve path to send them through.
Where next
Section titled “Where next”Back to the SDK map. For an application, read on in this order: loading and pinning for reproducible versions, facts and context for the call shape, and results and errors for the answer shape.
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.