Markdown for LLMs
Python
The source Markdown for this article. Copy it into your assistant or download it as a text file.
# 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
Two routes, different promises:
```bash
# Reproduce the pinned example: the versions in the compatibility record.
pip install arxo==0.2.0 arxo-canon-bgb-fristen==0.1.1
```
```bash
# Install current versions: newest compatible pair, not the pinned one.
pip install arxo arxo-canon-bgb-fristen
```
The 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
The frist scenario from the section map, checked with `truth`:
```python
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 build
r.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`](#open)
and how it resolves a spec, the [`LawPackage`](#lawpackage) methods,
the [`Answer`](#answers) fields, the [errors](#errors), and the
operations Python [does not ship](#what-python-does-not-ship).
## Open
```python
def open(spec: str, options: dict[str, Any] | None = None) -> LawPackage:
```
```python
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:
```python
def parse_spec(spec: str | None) -> dict[str, str]:
```
```python
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@version
```
Resolution 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
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.
```python
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`:
```python
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
```python
@dataclass
class 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 = None
```
`document` 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
```python
fristen.passport().in_model("frist_ende") # True
fristen.passport().inModel("frist_ende") # True, JavaScript name
```
`in_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
```python
class LawClientError(Exception): ... # common ancestor of transport failures
class IntegrityError(LawClientError): ...
class ProtocolError(LawClientError): ...
class RpcError(LawClientError): ...
class TransportError(LawClientError): ...
class ValidationError(LawClientError): ...
class FactError(Exception): ... # path, nearest
class PackageNotFoundError(Exception): ... # package_name, version
```
Four different situations, three different treatments:
- **Absent fact**: a well-formed fact simply not sent. No
exception: the answer is a value, `COMPUTED` with `NEITHER`
(removing `frist_dauer_tage` from 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
missing `legalTime`. Raises `FactError` with the failing path.
- **Unknown relation**: a predicate the canon never declared
(`unknown relation "no_such_relation"`). Raises `FactError`.
- **Missing package**: no source has the pinned canon. Raises
`PackageNotFoundError` naming 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
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
- The evaluator is the bundled `law_wasm_core.wasm`, hosted through
`wasmtime`; pass `wasmPath` in the options to load another build.
- The calendar comes from the installed canon when it ships one, else
from the `calendar` option.
- A case already lowered (assertions with `kind: 'assertion'`) is
passed through; otherwise the facade assembles the case from
`answers` and wraps each bare value by its declared type.
- The only network call in this release is `explain` through the MCP
client made on first use — unless the package was opened offline,
which raises `TransportError` instead. Queries (`truth`,
`why_not`, `collect`, `deadline`, `calc`, `positions`) never
leave the process: there is no serve path to send them through.
## Where next
Back to the [SDK map](/build/sdk/). For an application, read on in
this order: [loading and pinning](/build/sdk/loading-and-pinning/)
for reproducible versions, [facts and context](/build/sdk/facts-and-context/)
for the call shape, and [results and errors](/build/sdk/results-and-errors/)
for the answer shape.