docs← Back to article

Markdown for LLMs

TypeScript

The source Markdown for this article. Copy it into your assistant or download it as a text file.

Download this articlePlain text ↗
# 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

Two routes, different promises:

```bash
# Reproduce the pinned example: the versions in the compatibility record.
npm i @arxo/law@0.3.3 @arxo/canon-bgb-fristen@0.1.5
```

```bash
# 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.

## Lifecycle in four lines

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

```ts
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](#entry-points) for Node and the browser, the
[`open` overloads](#open) and their options, and the full
[`LawPackage`](#lawpackage) declarations with the
[errors](#errors) they raise.

## Entry points

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:

```ts
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

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

```ts
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>;
```

```ts
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:

```ts
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

```ts
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.

```ts
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

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

```ts
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:

```js
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

```ts
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

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

```js
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

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

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