Skip to content
docs
Arxo ↗

TypeScript

For LLMs10 sections

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.

Two routes, different promises:

Terminal
# Reproduce the pinned example: the versions in the compatibility record.
npm i @arxo/law@0.3.3 @arxo/canon-bgb-fristen@0.1.5
Terminal
# Install current versions: newest compatible pair, not the pinned one.
npm i @arxo/law @arxo/canon-bgb-fristen

The 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.

A package goes from open, to query, to reading the answer, to archive:

TypeScript
const fristen = await open('de.bgb.fristen@0.1.0'); // open
const r = await fristen.truth('frist_ende', ['frist', '2026-03-20'], caseInput); // query
console.log(r.evaluationStatus, r.truthStatus); // read
r.document; // archive: canonical bytes

document 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.

The package map exposes one surface per host:

JSON
{
".": "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:

TypeScript
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.

TypeScript
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>;
TypeScript
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:

TypeScript
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.

TypeScript
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.

TypeScript
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>;
};

Two errors belong to the facade; the transport family is shared with the client package:

TypeScript
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:

JavaScript
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.

TypeScript
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.

The frist scenario from the section map, asked through collect:

JavaScript
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.

  • Predicate names complete only for installed canons that register their relations; otherwise the argument stays a plain string.
  • explain needs the host: it calls law_explain over the carried document, so an offline package cannot explain.
  • verbalize needs the full engine and a template pack; local answers only.
  • focusedTruth returns a slice by the question’s cone, not an audit document: it has no full result hash.

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.