TypeScript
The facade is @arxo/law at version 0.3.3 with the canon package
@arxo/canon-bgb-fristen at 0.1.5, which carries the canon
de.bgb.fristen at 0.1.0. You need Node 20 or newer — 20 is the
floor, and a supported LTS (24 recommended) is the route for a new
setup. Everything below runs locally after install; the calls that
reach the network say so in their own chapter.
Install
Section titled “Install”Two routes, different promises:
# Reproduce the pinned example: the versions in the compatibility record.npm i @arxo/law@0.3.3 @arxo/canon-bgb-fristen@0.1.5# Install current versions: newest compatible pair, not the pinned one.npm i @arxo/law @arxo/canon-bgb-fristenThe first route installs the pair listed on the compatibility page.
The second may install newer releases; inside the starter prefer
npm ci, which reproduces the whole pinned tree from the lockfile,
and re-run the probe on the compatibility page when a version
changes.
Lifecycle in four lines
Section titled “Lifecycle in four lines”A package goes from open, to query, to reading the answer, to archive:
const fristen = await open('de.bgb.fristen@0.1.0'); // openconst r = await fristen.truth('frist_ende', ['frist', '2026-03-20'], caseInput); // queryconsole.log(r.evaluationStatus, r.truthStatus); // readr.document; // archive: canonical bytesdocument is a Uint8Array of canonical bytes on a local answer —
what you store or hand to a reviewer. A host answer carries document: null and keeps the raw payload in host; the results chapter covers
both shapes.
The rest of the page is the detail behind these four lines: the
entry points for Node and the browser, the
open overloads and their options, and the full
LawPackage declarations with the
errors they raise.
Entry points
Section titled “Entry points”The package map exposes one surface per host:
{ ".": "the facade: open, LawPackage, errors", "./browser": "no Node-only imports; wasm bytes are passed in", "./router": "route a question in your own words to a canon", "./case-package": "open a saved case package from disk (Node)", "./arxo": "containers: pin several canons into one world", "./task-guides": "guided passages over a canon (Node)"}The browser entry takes the engine bytes and the canons from the bundle instead of the file system:
export type BrowserOpenOptions = Omit<OpenOptions, 'wasmPath' | 'cacheDir' | 'local'> & { /** `law_wasm_core.wasm`: a fetch Response, raw bytes, or a URL to fetch. Required. */ wasm: Response | ArrayBuffer | Uint8Array | string | URL; /** Canons the bundle carries, keyed `name@version`. */ canons?: Record<string, { ir: unknown; calendar?: string; meta?: { passport?: PassportMeasure } }>; /** contentHash pins for the Cache Storage lookup, keyed `name@version`. */ pins?: Record<string, string>; /** A CacheStorage to use instead of `globalThis.caches`. */ caches?: CacheStorage; local?: { packages?: Record<string, { ir: unknown; calendar?: string }> };};open resolves a pinned spec to a package object. The spec always
carries a version; the typed overload completes predicate names from
the installed canon’s registry entry.
export function open(spec: string, options: OpenOptions & { serve: ServeOptions }): Promise<ServePackage>;export function open<N extends string>( spec: `${N}@${string}`, options?: OpenOptions,): Promise<LawPackage<RelationsOf<N>>>;export function open(spec: string, options?: OpenOptions): Promise<LawPackage>;import { open } from '@arxo/law';
const fristen = await open('de.bgb.fristen@0.1.0');After open, the predicate argument is typed by the canon: the editor
completes frist_ende, and a name the canon does not know fails type
checking before anything runs. A StableId of the form urn:...#local
is always accepted and checked at run time.
The options select where the canon comes from and how strictly the facade stays offline:
export type OpenOptions = { /** Connect to `law serve` without loading the local wasm engine. */ serve?: ServeOptions; remote?: boolean; offline?: boolean; fetch?: typeof fetch; cacheDir?: string; registryUrl?: string; mcpEndpoint?: string; /** The act key the MCP host uses (`metadata.act`), when it differs from the canon name and no installed canon names it. */ hostPackage?: string; wasmPath?: string | URL; calendar?: string; contentHash?: string; passport?: PassportMeasure; engine?: unknown; local?: { root?: string; map?: Record<string, { clir: string; calendar?: string; passport?: string }>; packages?: Record<string, { ir: unknown; calendar?: string; meta?: unknown }>; };};With serve, open returns a ServePackage instead: no local engine
is loaded, and questions go to the server over HTTP. See the loading
chapter for what each source means.
LawPackage
Section titled “LawPackage”export declare class LawPackage<Rel extends string = string> { readonly name: string; readonly version: string; readonly source?: string; readonly via: 'local' | 'mcp'; /** The act key the MCP host uses for this package. */ readonly hostKey: string; questions(lang?: string): FormManifest; passport(): Passport; ask(request: AskRequest): Promise<Answer>; truth(predicate: Rel, args: FactArg[], caseInput: CaseInput): Promise<Answer>; /** * `focused_truth`: the same answer as `truth`, the proof and * issues by the question's cone only. Not an audit document. */ focusedTruth(predicate: Rel, args: FactArg[], caseInput: CaseInput): Promise<Answer>; whyNot(predicate: Rel, args: FactArg[], caseInput: CaseInput): Promise<Answer>; collect(predicate: Rel, args: FactArg[] | { args: FactArg[]; free?: unknown }, caseInput: CaseInput): Promise<Answer>; /** Calendar operation: `date` plus `days`/`unit`; `policy` defaults to the case's `deadlinePolicy`. */ deadline( opArgs: { date: string; days?: number; unit?: 'business_day' | 'calendar_day'; afterTime?: string; policy?: unknown; suspensions?: Array<{ start: string; end: string }> }, caseInput: CaseInput, ): Promise<Answer>; calc(term: unknown, caseInput: CaseInput): Promise<Answer>; positions(caseInput: CaseInput): Promise<Answer>; /** * Term unfolding: the tree of producers of a predicate * over the package's world and the text the engine's `unfold` prints. No rule is * executed. `predicate`: a local name, `package::name`, or a full id. */ unfold(predicate: Rel | string, options?: { lang?: string; depth?: number; quote?: number; fold?: boolean }): { tree: unknown; text: string }; /** The host's `law_explain` over the document this answer carries. Needs the host. */ explain(answer: Answer, options?: { nodeId?: string }): Promise<Explanation>; /** * Answer verbalization: question and answer as controlled text * by the template pack — the artifact `law.answer-verbalization/0.1`. Local answers only; * needs the full engine (`options.engine`, or loaded lazily in Node). */ verbalize(answer: Answer, options?: { detail?: 'full' | 'brief'; pack?: unknown; engine?: unknown }): Promise<AnswerVerbalization>;}ask takes the uniform request; the named methods are wrappers over
it. The query chapter says when to use each kind.
export type AskRequest = { kind: 'truth' | 'focused_truth' | 'why_not' | 'collect' | 'calendar_op' | 'term' | 'positions'; predicate?: string; args?: FactArg[]; caseInput: CaseInput; queryId?: string; caseName?: string; extra?: Record<string, unknown>;};Errors
Section titled “Errors”Two errors belong to the facade; the transport family is shared with the client package:
export class FactError extends Error { path?: string; nearest?: string[]; constructor(message: string, details?: { path?: string; nearest?: string[] } & Record<string, unknown>);}
export class PackageNotFoundError extends Error { packageName?: string; version?: string; constructor(message: string, details?: { name?: string; version?: string } & Record<string, unknown>);}A misspelt predicate 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", ...]The shared client errors are LawClientError (the common ancestor),
TransportError, ProtocolError, RpcError, ValidationError, and
IntegrityError. The results chapter tables every class with the
condition that raises it.
Answers
Section titled “Answers”export type Answer = LocalAnswer | RemoteAnswer;A local answer carries document: Uint8Array and via: 'local'; a
host answer carries document: null, via: 'mcp', and the raw payload
in host. Both carry evaluationStatus, hashes, and, where the kind
produces them, truthStatus, value, whyNot, and missingInputs.
Example: compute the end date
Section titled “Example: compute the end date”The frist scenario from the section map, asked through collect:
const end = await fristen.collect('frist_ende', ['frist', '?end'], { 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'] }, ],});end.evaluationStatus; // 'COMPUTED'end.value[0].value; // '2026-03-20'Ask the same question with a wrong candidate date through truth and
the answer is COMPUTED with NEITHER; drop one fact and it is still
COMPUTED with NEITHER, with two blockers under whyNot.
Limits
Section titled “Limits”- Predicate names complete only for installed canons that register their relations; otherwise the argument stays a plain string.
explainneeds the host: it callslaw_explainover the carried document, so an offline package cannot explain.verbalizeneeds the full engine and a template pack; local answers only.focusedTruthreturns a slice by the question’s cone, not an audit document: it has no fullresulthash.
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.