# API reference

Every export of geospec and the geospec-engine wheel, with options, defaults, results, errors and constants in TypeScript and Python.

Canonical page: https://www.geospec.xyz/docs/api

`npm install geospec@beta geospec-engine` gives TypeScript the assertions and the engine; `pip install geospec-engine` gives Python the `geospec` package with the engine inside. [Install](https://www.geospec.xyz/docs/install.mdx) covers the engine artifact each host loads.

The examples run beside the demo models [`plate-with-through-bore.step`](/demo/plate-with-through-bore.step), a 40 × 40 × 20 mm STEP plate with a 10 mm bore, and [`involute-gear.glb`](/demo/involute-gear.glb), a gear mesh in millimetres.

## Every option and its default [#every-option-and-its-default]

**TypeScript**

```ts
import { readFile } from 'node:fs/promises';
import { expectGeo } from 'geospec';
import { createModelLoader } from 'geospec/model';
import { Engine } from 'geospec-engine/node';

const loadModel = createModelLoader({
  // required; Engine takes the
  // cache, CPU permits and limits
  engine: new Engine(),
  // required for a path
  readSource: async (path) => new Uint8Array(await readFile(path)),
  // default for every load
  format: 'glb',
});
const plate = await loadModel({
  // a path, or the bytes
  source: 'plate-with-through-bore.step',
  // default 'glb'; also 'stp'
  format: 'step',
  // engine default: 8,000,000
  workUnitLimit: 8_000_000,
});
const gear = await loadModel({
  source: 'involute-gear.glb',
  // default 'm' for a mesh file
  sourceUnit: 'mm',
});
const size = expectGeo(plate, {
  // default: the subject's,
  // then the engine's
  workUnitLimit: 8_000_000,
}).toHaveBoundingBox({
  size: { x: 40, y: 40, z: 20 },
  // default 0.02 mm
  tolerance: 0.02,
});
console.log(size.report.claimId, size.report.status, size.report.claim.workUnitBudget); // geospec-claim-1 passed 8000000
await loadModel.dispose();
```

**Python**

```python
from geospec import GeoSpecEngine, expect_geo, load_model

engine = GeoSpecEngine(
    # default None, and file=
    # refuses until it is set
    model_root=".",
    # engine default: 8_000_000
    work_unit_budget=None,
    # up to the host's CPU count
    execution_permits=None,
    # default: the entry profile
    limits=None,
    # set both to cache on disk
    cache_root=None,
    project_root=None,
    # required for
    # load_model(code=...)
    model_loader=None,
)
with engine:
    plate = load_model(
        # under model_root; or
        # source=bytes | path
        file="plate-with-through-bore.step",
        # default "glb"; also
        # "gltf" and "stp"
        format="step",
        # default "auto" for STEP
        # and "m" for a mesh
        source_unit="auto",
        # glTF buffers: dicts with
        # "name" and "source"
        resources=(),
        # for STEP: {"name": ...},
        # which changes the hash
        ingest_options=None,
        # provenance name;
        # default: file
        name="plate.step",
    )
    report = expect_geo(plate, claim_id="size").to_have_bounding_box(size={"x": 40, "y": 40, "z": 20})
    print(report.passed, report.result["status"], report.claim["workUnitBudget"])  # True passed 8000000
```

## Entry points [#entry-points]

**TypeScript**

* `geospec`: `expectGeo`, `geoSpecMatcherNames` and the types; `describe`, `it` and `test` for spec files.
* `geospec/model`: `loadModel`, `createModelLoader`, `GeoSpecModelLoadError`.
* `geospec/assertion-client`: `createGeoSpecAssertionClient`, `GeoSpecAssertionError`, `geoSpecMaximumWorkUnitLimit`.
* `geospec/runner/native`: `createGeoSpecNativeModelLoader`.
* `geospec/vitest`: `setupGeoSpecVitest`, `installGeoSpecVitest`, `createGeoSpecVitestAdapter`.
* `geospec-engine`: `Engine`, `initialize`, `canonicalize`, `ProtocolError` (WASM).
* `geospec-engine/node`: `Engine`, `canonicalize`, `ProtocolError` (native addon).

**Python**

`geospec` exports 52 names: the nine documented here, `GeoSpecMatchers`, `GeoSpecLoadModelOptions` and the typed dicts behind them; `geospec_engine_native` is the engine over `bytes`, and the wheel registers the pytest plugin `geospec`.

The engine's byte protocol is for embedders and lives in the [engine package README](https://github.com/taucad/geospec/blob/main/packages/geospec-engine/README.md).

## Load a model [#load-a-model]

### loadModel [#loadmodel]

Read a model, admit it to the engine and return the subject claims measure.

**TypeScript**

```ts
loadModel(options: LoadModelSourceOptions): Promise<GeoSpecSubject>
```

* **Parameters:** as annotated in the options block, plus `resources`, named glTF buffers, and `name`, `path` and `parameters` for the load record.
* **Returns:** an opaque `GeoSpecSubject`, alive until `loadModel.dispose()`.
* **Throws:** `GeoSpecModelLoadError`; its first diagnostic's `code` names the cause: `GEOSPEC_NATIVE_MODEL_FORMAT_UNSUPPORTED` outside `glb`, `step` and `stp`, `GEOSPEC_MODEL_OPTION_UNSUPPORTED` for `mesh`, `stepStreaming`, the mesh tolerances and `sourceUnit` on STEP, `GEOSPEC_NATIVE_RUNTIME_UNAVAILABLE` for `{ file }` without a CAD runtime.

```ts
const plate = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });
const gear = await loadModel({ source: 'involute-gear.glb', sourceUnit: 'mm' });
const size = expectGeo(plate).toHaveBoundingBox({ size: { x: 40, y: 40, z: 20 } });
console.log(
  size.report.status,
  size.report.evidence.measured.size,
  size.report.evidence.subjectContentHash.slice(0, 16),
);
// passed [ 40, 40, 20 ] fb622c854152ce81
```

**Python**

```python
load_model(*, file=..., source=..., format="glb", source_unit=..., resources=(), ingest_options=None, name=...) -> GeoSpecSubject
```

* **Parameters:** as annotated in the options block: exactly one of `file` and `source`; `format`; `source_unit`; `resources`, each with exactly `name` and `source`; `ingest_options`; `name`.
* **Returns:** a frozen `GeoSpecSubject` (`engine`, `slot`, `identity_field`, `identity`, `provenance`), alive until `close()` on it or on the engine.
* **Raises:** `GeoSpecAssertionError` with `GEOSPEC_MODEL_HOST_UNAVAILABLE`, `GEOSPEC_MODEL_PATH_OUTSIDE_ROOT`, `GEOSPEC_MODEL_FORMAT_UNSUPPORTED`, `GEOSPEC_MODEL_OPTION_UNSUPPORTED` or `invalid-field`; `FileNotFoundError` for a missing file; `TypeError` for `file` with `source`.

```python
plate = load_model(file="plate-with-through-bore.step", format="step")
gear = load_model(file="involute-gear.glb", source_unit="mm")
print(plate.provenance["primary"]["sha256"][:16], plate.identity_field, gear.identity[:16])  # fb622c854152ce81 subjectHash a9913a341848cb6b
```

### createModelLoader [#createmodelloader]

**TypeScript**

```ts
createModelLoader(defaults: CreateModelLoaderOptions): ManagedGeoSpecModelLoader
```

* **Parameters:** `engine`; `readSource`, required for a path source; `format` and the mesh options as defaults; `projectPath`, `runtime` and `sourceAdapters` for code models.
* **Returns:** `loadModel` with `dispose()`, which releases every subject it admitted.
* **Throws:** nothing on creation; a path load without `readSource` throws `GEOSPEC_NATIVE_SOURCE_READER_UNAVAILABLE`.

```ts
const plate = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });
await loadModel.dispose();
expectGeo(plate).toHaveVolume({ value: 30429.2 }); // TypeError GEOSPEC_SUBJECT_INVALID: reload it within its live scope
```

**Python**

`GeoSpecEngine` is the loader: it owns its subjects and runs on the thread that created it.

```python
GeoSpecEngine(*, model_root=None, work_unit_budget=None, execution_permits=None, cache_root=None, project_root=None, model_loader=None)
```

* **Parameters:** as in the options block above; `cache_root` and `project_root` go together.
* **Returns:** the engine; as a context manager it is `load_model`'s engine and closes on exit, releasing every subject.
* **Raises:** `ValueError` for a budget that is not a positive safe integer, permits outside 1 to the CPU count, or a cache root alone; `RuntimeError` off the owner thread or after `close()`.

```python
with GeoSpecEngine(model_root=".") as engine:
    print(engine.protocol_version, engine.registry_version, len(engine.capabilities), engine.configuration["defaultWorkUnitBudget"])  # 3 5 32 8000000
```

## State a claim [#state-a-claim]

### expectGeo [#expectgeo]

Start a chain on a subject; each matcher call is one claim, answered in that call. The [matcher reference](https://www.geospec.xyz/docs/reference/overview.mdx) lists the 26 matchers and their fields; a scalar matcher takes `{ value, tolerance }`.

**TypeScript**

```ts
expectGeo(subject: GeoSpecSubject, options?: GeoSpecExpectOptions): GeoSpecMatcher
```

* **Parameters:** the subject; `workUnitLimit`, this chain's budget.
* **Returns:** the matchers and `.not`, each returning a `GeoSpecAssertion` with the engine's `report`; claim ids count `geospec-claim-N` per process, shared with the assertion client.
* **Throws:** `GeoSpecAssertionError` for a failed or refused claim, carrying the same report; `ProtocolError` `invalid-claim` for a plan the engine rejects, such as a negated query; `TypeError` for a subject outside its live scope or a `workUnitLimit` outside 1 to the host's maximum.

```ts
const noHole = expectGeo(plate).not.toHaveCircularHole({ diameter: 12 });
console.log(noHole.report.claimId, noHole.report.polarity, noHole.report.status); // geospec-claim-1 negative passed
expectGeo(gear, { workUnitLimit: 100 }).toHaveBoundingBox({ size: { z: 5 } }); // GeoSpecAssertionError: refused, MATCHER_TIMEOUT, details.used 3744
```

**Python**

```python
expect_geo(subject, *, claim_id=None) -> GeoSpecMatchers
```

* **Parameters:** the subject; `claim_id`, default `geospec-claim-N` from one counter per engine.
* **Returns:** the matchers and `.not_`, each returning a `GeoSpecAssertionReport`; a matcher is the snake\_case registry name and takes keyword fields with their camelCase names (`toleranceMm=`), or one positional dict.
* **Raises:** `GeoSpecAssertionError` for a failed or refused claim, and with code `invalid-claim` for a plan the engine rejects; `AttributeError` for a name outside the registry; `TypeError` for a positional dict with keyword fields.

```python
report = expect_geo(plate).to_have_bounding_box(size={"x": 40, "y": 40, "z": 20})
print(report.claim["claimId"], report.result["status"], report.result["evidence"]["measured"]["size"])  # geospec-claim-1 passed [40, 40, 20]
try:
    expect_geo(plate).to_have_circular_hole(diameter=12)
except GeoSpecAssertionError as error:
    print(error.code, error.result["status"], error)  # GEOSPEC_FEATURE_MISMATCH failed No circular hole matches diameter 12 within 0.02 mm.
expect_geo(gear).not_.to_have_circular_hole(diameter=4)  # GeoSpecAssertionError: refused, GEOSPEC_UNSUPPORTED_EVIDENCE, under either polarity
```

`evaluate_geo(subject, capability, *arguments, claim_id=None, polarity="positive")` runs one matcher by registry name and returns the report whatever the status, with `passed` `False` for a refusal; a bad payload raises `geospec_engine_native.ProtocolError`, unwrapped.

## Query geometry [#query-geometry]

The five queries, `analyzeMesh`, `analyzeBrep`, `inspectGeometry`, `analyzeMeshOverlap` and `queryPmi`, report what the engine measured, take positive polarity only, and refuse when the evidence they read is missing or cannot decide; a query never fails. [Query geometry](https://www.geospec.xyz/docs/guides/query-geometry.mdx) reads the reports.

### createGeoSpecAssertionClient [#creategeospecassertionclient]

**TypeScript**

A client runs chains and queries outside a test runner, on subjects from `createGeoSpecNativeModelLoader({ engine, readSource })` in `geospec/runner/native`: its `loadModel` returns `{ subjectHash, load }`, `releaseAll()` releases them, and the root `expectGeo` rejects them with `TypeError` `GEOSPEC_SUBJECT_INVALID`.

```ts
createGeoSpecAssertionClient(options: GeoSpecAssertionClientOptions): GeoSpecAssertionClient
```

* **Parameters:** `engine`; `workUnitLimit`, every claim's budget unless the chain or the subject sets one; `claimId(matcher, sequence)`; `evidenceProfile`, `complete` or `bounded`; `subjectSlot`; `maximumWorkUnitLimit`.
* **Returns:** `expectGeo(subject, options?)`, whose matchers return the `GeoSpecCanonicalClaimReport` directly, and `query`.
* **Throws:** as `expectGeo`; a subject of another engine, or one released by `releaseAll()`, throws `ProtocolError` `invalid-claim`.

```ts
import { createGeoSpecAssertionClient } from 'geospec/assertion-client';
import { createGeoSpecNativeModelLoader } from 'geospec/runner/native';

const loadModel = createGeoSpecNativeModelLoader({ engine, readSource });
const { expectGeo, query } = createGeoSpecAssertionClient({ engine });
const plate = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });
const volume = expectGeo(plate).toHaveVolume({ value: 30429.2 });
console.log(plate.subjectHash.slice(0, 16), volume.claimId, volume.status, volume.claim.workUnitBudget); // da21b0d6142fc5fb geospec-claim-1 passed 8000000
```

**Python**

Python needs no client: `expect_geo`, `evaluate_geo` and `query_geo` use the engine that owns the subject.

### query [#query]

**TypeScript**

```ts
client.query(options: GeoSpecQueryOptions): Promise<GeoSpecCanonicalClaimReport>
client.query(options: MinimumDistanceQuery): Promise<MinimumDistanceResult>
```

* **Parameters:** `capability`; `subject`, from the native loader or a bare `{ subjectHash }`; `payload`, omitted for `analyzeMesh` and `analyzeBrep`; `claimId`, which leaves the shared counter alone.
* **Returns:** the report; `minimumDistance` returns `{ status: 'complete', fact }` or `{ status, code, message }` (`unsupported-evidence` without AP242 provenance).
* **Throws:** `ProtocolError` `invalid-claim` for a plan the engine rejects; `TypeError` for a subject without exactly one of `subjectHash` and `contentHash`.

```ts
const brep = await query({ capability: 'analyzeBrep', subject: plate });
console.log(brep.claimId, brep.status, Object.keys(brep.evidence).join(',')); // geospec-claim-2 passed brep,diagnostics,success
await loadModel.releaseAll();
```

**Python**

```python
query_geo(subject, capability, payload=None, *, claim_id=None) -> GeoSpecAssertionReport
```

* **Parameters:** the subject; a query name; `payload`, a mapping whose regular-expression selectors are `GeoSpecRegex(pattern, flags="")`, since a compiled `re.Pattern` raises `TypeError`; `claim_id`.
* **Returns:** the report.
* **Raises:** `ValueError` for a capability that is not a query; `geospec_engine_native.ProtocolError` for an unknown payload field, unwrapped.

```python
brep = query_geo(plate, "analyzeBrep")
print(brep.result["status"], sorted(brep.result["evidence"]))  # passed ['brep', 'diagnostics', 'success']
print(query_geo(gear, "analyzeBrep").diagnostics[0]["code"])  # GEOSPEC_BREP_EVIDENCE_UNAVAILABLE
query_geo(plate, "toHaveVolume")  # ValueError: GeoSpec capability 'toHaveVolume' is not a query.
```

## Run specs [#run-specs]

**TypeScript**

`geospec run`, the command in `geospec`, runs every `*.geospec.ts` or `*.geospec.js` file under the directory it is given, with no configuration file: it compiles each spec, supplies the engine and owns the subjects, so a spec has no hooks. A spec imports `describe`, `it` and `expectGeo` from `geospec` and `loadModel` from `geospec/model`, which takes no engine and reads a path relative to that directory; it never imports `geospec-engine`.

```ts title="plate.geospec.ts"
import { describe, expectGeo, it } from 'geospec';
import { loadModel } from 'geospec/model';

describe('plate', () => {
  it('is 40 x 40 x 20 with a 10 mm bore', async () => {
    const plate = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });
    expectGeo(plate).toHaveBoundingBox({ size: { x: 40, y: 40, z: 20 } });
    expectGeo(plate).toHaveCircularHole({ diameter: 10 });
  });

  it('refuses a hole claim on the gear mesh', async () => {
    const gear = await loadModel({ source: 'involute-gear.glb', sourceUnit: 'mm' });
    expectGeo(gear).toHaveCircularHole({ diameter: 4 });
  });
});
```

```console
$ npx geospec run .
FAIL plate.geospec.ts
  fail plate > refuses a hole claim on the gear mesh
    GEOSPEC_UNSUPPORTED_EVIDENCE: expectGeo(...).toHaveCircularHole() needs exact BRep circular-hole evidence, which this subject does not carry.
1 passed, 0 failed, 2 selected
```

The exit code is the verdict: 0 only when every selected test passed; a failed test, a refused claim, a run-level issue or an empty selection exits 1. A refused claim leaves its test `inconclusive`, counted in neither number on the last line. `--json` prints one report instead: `success`, an `accounting` block with `passed`, `failed`, `inconclusive`, `skipped` and `notRun`, and for each test its `status`, `diagnostics` and claim `reports`.

* `--file <path>`, `--include <glob>` and `--exclude <glob>` select files, `-t <regex>` selects `suite > test` names; a filtered run proves the selected scope only.
* `--workers [n]` runs a worker pool, sized to the host's CPUs without `n`.
* `--test-timeout <ms>`, `--shard-timeout <ms>` and `--matcher-wall-backstop <ms>` are wall-clock watchdogs, off by default; a timeout is no geometry verdict.
* `--bail` stops after the first failing file; `--forensic` appends timing lines.

Under Vitest, `geospec/vitest` registers the 26 matchers on `expect`. `setupGeoSpecVitest(options)` creates an assertion client from the `createGeoSpecAssertionClient` options and installs its matchers in the current file; `installGeoSpecVitest(client)` installs an existing client's, and `createGeoSpecVitestAdapter(client)` returns `{ matchers, flush }` without installing them. A matcher returns a promise, so await it. A failed or refused claim fails the test with the claim's result as its message, and a refusal fails under `.not` too.

```ts title="plate.test.ts"
import { readFile } from 'node:fs/promises';
import { afterAll, expect, it } from 'vitest';
import { createModelLoader } from 'geospec/model';
import { setupGeoSpecVitest } from 'geospec/vitest';
import { Engine } from 'geospec-engine/node';

const engine = new Engine();
const loadModel = createModelLoader({
  engine,
  readSource: async (source) => new Uint8Array(await readFile(source as string)),
});
setupGeoSpecVitest({ engine });
afterAll(async () => {
  await loadModel.dispose();
  engine.close();
});

it('refuses a hole claim on the gear mesh', async () => {
  const gear = await loadModel({ source: 'involute-gear.glb', sourceUnit: 'mm' });
  await expect(gear).toHaveCircularHole({ diameter: 4 });
});
```

`npx vitest run` reports `1 failed`; the message under the test is the result JSON, `GEOSPEC_UNSUPPORTED_EVIDENCE` first among its diagnostics.

Any other runner works with an explicit engine: a failed or refused claim throws `GeoSpecAssertionError`, failing the test with the engine's message; release the subjects in an `after` hook. `node --test plate.test.mjs` reports `fail 1`.

```ts title="plate.test.mjs"
import { readFile } from 'node:fs/promises';
import { after, test } from 'node:test';
import { expectGeo } from 'geospec';
import { createModelLoader } from 'geospec/model';
import { Engine } from 'geospec-engine/node';

const engine = new Engine();
const loadModel = createModelLoader({ engine, readSource: async (path) => new Uint8Array(await readFile(path)) });
after(async () => {
  await loadModel.dispose();
  engine.close();
});

test('a hole claim on the gear mesh is refused', async () => {
  const gear = await loadModel({ source: 'involute-gear.glb', sourceUnit: 'mm' });
  expectGeo(gear).toHaveCircularHole({ diameter: 4 });
});
```

**Python**

The wheel's pytest plugin gives each test an engine, created on its first `load_model` and closed at teardown, with `model_root` at pytest's `rootpath`; `GeoSpecAssertionError` extends `AssertionError`, so a failed or refused claim fails the test.

```python title="test_plate.py"
import pytest
from geospec import GeoSpecAssertionError, expect_geo, load_model


def test_plate():
    plate = load_model(file="plate-with-through-bore.step", format="step")
    expect_geo(plate).to_have_bounding_box(size={"x": 40, "y": 40, "z": 20})
    with pytest.raises(GeoSpecAssertionError) as raised:
        expect_geo(plate).to_have_circular_hole(diameter=12)
    assert raised.value.code == "GEOSPEC_FEATURE_MISMATCH"
```

```console
$ python -m pytest test_plate.py -q
.                                                                        [100%]
1 passed in 0.02s
```

Override the `geospec_engine_factory` fixture in `conftest.py` to set every test's `model_root`, `work_unit_budget` or `limits`.

## Engines [#engines]

### initialize [#initialize]

**TypeScript**

Load and compile the WASM module once, before constructing an `Engine` from `geospec-engine`; the `node` entry needs no initialization.

```ts
initialize(input?: WasmInput, execution?: WasmExecution): Promise<void>
```

* **Parameters:** `input`, the module as bytes, a `Response`, a `URL` or a string, or omitted for the packaged module; `execution`, single-threaded by default.
* **Returns:** a promise resolved once the module is ready.
* **Throws:** `ProtocolError` `invalid-request` for module bytes with no initializer.

```ts
import { Engine, initialize } from 'geospec-engine';

new Engine(); // ProtocolError invalid-request: Call initialize() before using the GeoSpec WASM engine.
await initialize();
const engine = new Engine();
```

**Python**

The wheel needs no initialization: `GeoSpecEngine` sends the `initialize` request itself and exposes its answer as `protocol_version`, `registry_version`, `canonical_profile`, `capabilities` and `configuration`, which carries `defaultWorkUnitBudget` and the engine's effective limits.

### Engine [#engine]

**TypeScript**

One engine owns its admitted subjects. The native addon, the WASM module and the wheel answer a claim with the same bytes: the plate's bounding-box result above is 532 canonical bytes, SHA-256 `57e6453a170bada5…`, from all three.

* **Parameters:** `new Engine(cacheOptions?, executionPermits?, limits?)` from `geospec-engine/node` takes an optional filesystem cache, `{ root, projectRoot }`, a caller-inclusive CPU allocation, default one, and the host limits; `new Engine(execution?, limits?)` from `geospec-engine` takes the product `initialize` loaded and the same limits.
* **Limits:** `maxSubjectBytes` (64 MiB), `maxResourceBytes` (64 MiB) and `maxTotalBinaryBytes` (128 MiB) bound the bytes one load admits; `maxVertices` (2,000,000), `maxTriangles` (4,000,000) and `maxOccurrences` (65,536) its decoded mesh and occurrence list; `maxRetainedSubjects` (32) the subjects one engine holds; `maxMeshBytes` (256 MiB) and `maxMeshEntries` (256) the bytes and the entries one analysis retains or uses as scratch. `maxSolidEntries` (256) bounds the CSG source solids the engine keeps for reuse across claims; an operand past it is admitted for its claim alone, never refused. The values in parentheses are the entry profile, `geospec-entry-config-v1`, which an omitted limit keeps; an engine with other values reports `geospec-host-config-v1-<id>`.
* **Returns:** the engine the loaders and clients take as `engine`.
* **Throws:** `ProtocolError` `invalid-request` for a limit that is not a positive integer up to 4,294,967,295, a `maxSubjectBytes` or `maxResourceBytes` above `maxTotalBinaryBytes`, or a name the engine does not know; after `close()`, every use throws the same code, `GeoSpec Node engine is closed.` or `GeoSpec WASM engine is closed.`

A load over an admission limit throws `ProtocolError` `limit-exceeded` with `capacity`, the numbers a refused claim carries in `details`. `geospec@0.1.0-beta.6` reads the default budget from the entry profile only, so a chain, model or client on an engine with its own limits sets `workUnitLimit` itself.

```ts
import { readFile } from 'node:fs/promises';
import { createModelLoader } from 'geospec/model';
import { Engine, ProtocolError } from 'geospec-engine/node';

const engine = new Engine(undefined, undefined, { maxTriangles: 600 });
const loadModel = createModelLoader({
  engine,
  readSource: async (source) => new Uint8Array(await readFile(source as string)),
});
try {
  await loadModel({ source: 'involute-gear.glb', sourceUnit: 'mm' }); // 624 triangles
} catch (error) {
  if (!(error instanceof ProtocolError) || !error.capacity) throw error;
  console.log(error.code, error.capacity.code, error.capacity.limit, error.capacity.required);
  console.log(error.message);
}
engine.close();
```

```console
limit-exceeded GEOSPEC_RESOURCE_LIMIT 600 624
The decoded mesh has 624 triangles; the configured triangle limit is 600 triangles.
```

**Python**

`GeoSpecEngine(limits={...})` takes the ten limits in snake\_case, `max_subject_bytes` to `max_retained_subjects`. An unknown name or a value that is not a positive integer raises `ValueError`; `max_subject_bytes` or `max_resource_bytes` above `max_total_binary_bytes` is a `ProtocolError` `invalid-request`. `engine.configuration` reports the effective values under `binaryAdmissionLimits`, `analysisRetentionLimits` and `maxRetainedSubjects`, and the profile ids as `configurationProfile` and `binaryAdmissionLimits["profileId"]`. A load over an admission limit raises `ProtocolError` `limit-exceeded` out of `load_model`, with `capacity_code` and `capacity_details`.

```python
from geospec import GeoSpecEngine, load_model
from geospec_engine_native import ProtocolError

with GeoSpecEngine(model_root=".", limits={"max_triangles": 600}) as engine:
    print(engine.configuration["configurationProfile"])
    try:
        load_model(file="involute-gear.glb", source_unit="mm")  # 624 triangles
    except ProtocolError as error:
        print(error.code, error.capacity_code, error.capacity_details)
        print(error)
```

```console
geospec-host-config-v1-1649e816d1e3c9a3
limit-exceeded GEOSPEC_RESOURCE_LIMIT {'limitClass': 'admission', 'unit': 'triangles', 'limit': 600, 'required': 624}
The decoded mesh has 624 triangles; the configured triangle limit is 600 triangles.
```

`GeoSpecEngine` also admits bytes directly:

* `ingest_subject(primary, *, format, coordinate_system, source_unit, output_unit, ingest_options=None, resources=(), slot="subject")` admits STEP, glTF or GLB bytes in a `z-up` or `y-up` frame, with `(name, bytes)` resources and `output_unit` `mm`.
* `ingest_mesh(mesh, *, slot="subject")` admits `mesh-buffer-v1` bytes, named by `contentHash`.

```python
from pathlib import Path

with GeoSpecEngine(model_root=".") as engine:
    part = engine.ingest_subject(
        Path("plate-with-through-bore.step").read_bytes(),
        format="step", coordinate_system="z-up", source_unit="auto", output_unit="mm",
        ingest_options={"name": "part"}, slot="part",
    )
    print(part.slot, part.identity_field, part.identity[:16])  # part subjectHash a53548710335079b
```

## Options [#options]

The type tables are from `geospec@0.1.0-beta.6`, the release that `geospec@beta` installs; every TypeScript example on this page ran on it. In Python, `load_model` takes `GeoSpecLoadModelOptions`, the snake\_case spellings of `LoadModelSourceOptions` plus `file` and `code`.

### LoadModelSourceOptions [#loadmodelsourceoptions]

**`LoadModelSourceOptions`** — Direct geometry-source model load options.

- **`source`** (`string | ArrayBuffer | Uint8Array<ArrayBuffer> | Blob | File | URL | MeshBufferSource | ReadableStream<Uint8Array<ArrayBuffer>> | AsyncIterable<Uint8Array<ArrayBuffer>>`, required) — Geometry bytes, path, browser file/blob, or in-memory mesh buffer.
- **`resources`** (`readonly { readonly name: string; readonly source: MeshSource | StepSource; }[] | undefined`, optional) — Named external resources consumed alongside the direct geometry bytes.
- **`format`** (`GeoSpecModelFormat | undefined`, optional) — Source geometry format. Defaults to `glb`.
- **`path`** (`string | undefined`, optional) — Source path recorded in provenance.
- **`name`** (`string | undefined`, optional) — Human-readable source name recorded in provenance.
- **`sourceUnit`** (`GeoSpecUnit | undefined`, optional) — Coordinate unit of raw GLB/glTF or mesh-buffer data before canonical millimetre normalization.
- **`parameters`** (`Record<string, unknown> | undefined`, optional) — Explicit parameters recorded in provenance.
- **`stepStreaming`** (`StepStreamingMode | undefined`, optional) — STEP reader strategy used for STEP sources.
- **`mesh`** (`boolean | undefined`, optional) — Whether STEP loading should also produce mesh evidence. Defaults to true.
- **`meshLinearTolerance`** (`number | undefined`, optional) — Linear tolerance used while meshing exact BRep evidence.
- **`meshAngularToleranceDegrees`** (`number | undefined`, optional) — Angular tolerance in degrees used while meshing exact BRep evidence.
- **`workUnitLimit`** (`number | undefined`, optional) — Default work-unit budget for every claim on this subject: a positive integer no greater than the host's maximum (default `geoSpecMaximumWorkUnitLimit`, 512 000 000). `expectGeo(subject, { workUnitLimit })` overrides it per chain. Recorded in the load evidence; it never changes the exported or admitted bytes.

### CreateModelLoaderOptions [#createmodelloaderoptions]

**`CreateModelLoaderOptions`** — Defaults accepted by {@link import ('./load-model.js').createModelLoader}.

- **`engine`** (`GeoSpecNativeModelEngine`, required) — Initialized compiled engine supplied by the host, never selected by an authored spec.
- **`readSource`** (`GeoSpecNativeSourceReader | undefined`, optional) — Rooted host reader for direct filesystem or URL sources.
- **`format`** (`GeoSpecModelFormat | undefined`, optional) — Geometry format to export when an individual call does not specify one.
- **`runtime`** (`GeoSpecRuntimeClient | GeoSpecRuntimeClientFactory | undefined`, optional) — Runtime client or lazy runtime factory.
- **`sourceAdapters`** (`readonly GeoSpecRuntimeSourceAdapter[] | undefined`, optional) — Source-specific runtime adapters, e.g. host-provided GPL-isolated kernels.
- **`projectPath`** (`string | undefined`, optional) — Project root used by runtime integrations.
- **`stepStreaming`** (`StepStreamingMode | undefined`, optional) — STEP reader strategy used for STEP sources or exports.
- **`mesh`** (`boolean | undefined`, optional) — Whether STEP loading should also produce mesh evidence. Defaults to true.
- **`meshLinearTolerance`** (`number | undefined`, optional) — Linear tolerance used while meshing exact BRep evidence.
- **`meshAngularToleranceDegrees`** (`number | undefined`, optional) — Angular tolerance in degrees used while meshing exact BRep evidence.

### GeoSpecExpectOptions [#geospecexpectoptions]

**`GeoSpecExpectOptions`** — Per-chain options accepted by `expectGeo(subject, options)`.

- **`workUnitLimit`** (`number | undefined`, optional) — Deterministic work-unit budget for every claim in this chain: a positive integer no greater than the host's maximum (default `geoSpecMaximumWorkUnitLimit`, 512 000 000). Omitted uses the subject's `loadModel` budget, then the host's, then the engine default. Raise it to retry a `MATCHER_TIMEOUT` refusal whose `details.used` exceeded `details.limit`.

### GeoSpecAssertionClientOptions [#geospecassertionclientoptions]

**`GeoSpecAssertionClientOptions`** — Flat construction options for a runner-independent native assertion client.

- **`claimId`** (`((matcher: GeoSpecMatcherName, sequence: number) => string) | undefined`, optional)
- **`engine`** (`GeoSpecNativeEngine`, required)
- **`evidenceProfile`** (`GeoSpecNativeEvidenceProfile | undefined`, optional) — Success-evidence profile of every claim and query; omitted means `complete`.
- **`subjectSlot`** (`string | undefined`, optional)
- **`workUnitLimit`** (`number | undefined`, optional)
- **`maximumWorkUnitLimit`** (`number | undefined`, optional) — The largest `workUnitLimit` a spec may author in `loadModel` or `expectGeo` options. Host policy: omitted uses `geoSpecMaximumWorkUnitLimit`, and a host provisioned for larger claims raises it. Must be a positive safe integer.

### GeoSpecQueryOptions [#geospecqueryoptions]

**`GeoSpecQueryOptions`** — One positive-only ancillary query. analyzeMesh/analyzeBrep use null payloads; inspectGeometry accepts selectors/evidence and analyzeMeshOverlap accepts pairs/tolerance as in the existing inspection and mesh APIs. queryPmi accepts optional maxRecords/maxOutputBytes limits and reports inventory, not compliance. Ordinary RegExp selectors are serialized by the shared protocol serializer. An explicit claimId leaves the automatic sequence unchanged. Queries return full reports, including failed/refused reports, without assertion errors.

- **`capability`** (`GeoSpecQueryCapability`, required)
- **`claimId`** (`string | undefined`, optional)
- **`payload`** (`unknown`, optional)
- **`subject`** (`GeoSpecNativeSubject`, required)

## Results [#results]

### GeoSpecAssertion [#geospecassertion]

A root `expectGeo` matcher returns a `GeoSpecAssertion`: the claim, the engine's `report` and the load it ran on.

**`GeoSpecAssertion`** — Geometry assertion collected from a GeoSpec test module.

- **`kind`** (`"toSatisfyRationalPlate" | "toSatisfyParallelPlaneDistance" | "boundingBox" | "connectedComponents" | "watertight" | "componentInterference" | "assemblyOccurrences" | "spatialRelationships" | "meshIntegrity" | "noDiagnostics" | "surfaceArea" | "volume" | "mass" | "centerOfMass" | "validBrep" | "topologyCounts" | "stepUnits" | "productStructure" | "planarFace" | "cylindricalFace" | "circularHole" | "circularHolePattern" | "chamferFeature" | "filletFeature" | "minimumWallThickness" | "voidContinuity"`, required) — Assertion kind.
- **`subject`** (`unknown`, required) — User-authored value passed to expectGeo().
- **`expected`** (`unknown`, required) — Expected geometry condition.
- **`passed`** (`boolean | undefined`, optional) — True when the assertion evaluated successfully.
- **`diagnostics`** (`GeometryDiagnostic[] | undefined`, optional) — Structured diagnostics from matcher evaluation.
- **`report`** (`GeoSpecCanonicalClaimReport | undefined`, optional) — Exact compiled assertion report, including core-owned bytes and polarity.
- **`loadId`** (`string | undefined`, optional) — The host load which admitted this assertion's subject; independent of equal geometry hashes.
- **`region`** (`GeoSpecLoadRegion | undefined`, optional) — The crop this assertion's subject was loaded with. Present only for a region load: the verdict holds for this millimetre box, never for the whole model.
- **`durationMs`** (`number | undefined`, optional) — Wall-clock cost of matcher evaluation in milliseconds (R1: budgeted matchers only).

### GeoSpecCanonicalClaimReport [#geospeccanonicalclaimreport]

The report is the record both languages keep: the canonical claim, plan and result bytes, the `claim` with its defaults filled in, and the `result` with `status`, `diagnostics` and `evidence`. [Reproduce a result](https://www.geospec.xyz/docs/guides/reproduce-a-result.mdx) hashes `canonicalResult` to compare runs.

**`GeoSpecCanonicalClaimReport`** — Full native assertion result with the exact core-owned bytes retained.

- **`canonicalClaim`** (`Uint8Array<ArrayBuffer>`, required)
- **`canonicalPlan`** (`Uint8Array<ArrayBuffer>`, required)
- **`canonicalResult`** (`Uint8Array<ArrayBuffer>`, required)
- **`claim`** (`Readonly<Record<string, JSONValue>>`, required)
- **`claimId`** (`string`, required)
- **`diagnostics`** (`readonly JSONValue[]`, required) — Engine diagnostics; a `MATCHER_TIMEOUT` row also carries `details.limit`, `details.used` and `details.maximumLimit`.
- **`evidence`** (`JSONValue | undefined`, optional)
- **`polarity`** (`GeoSpecClaimPolarity`, required)
- **`result`** (`Readonly<Record<string, JSONValue>>`, required)
- **`status`** (`GeoSpecCanonicalClaimStatus`, required)

In Python the same record is the frozen `GeoSpecAssertionReport`, with `canonical_*_bytes`, `claim`, `result`, `diagnostics`, `passed` and `provenance`.

### GeometryDiagnostic [#geometrydiagnostic]

A failed claim carries the matcher's mismatch code, such as `GEOSPEC_FEATURE_MISMATCH`; a refused claim carries the reason the [common refusals](https://www.geospec.xyz/docs/reference/overview.mdx#common-refusals) list, such as `GEOSPEC_UNSUPPORTED_EVIDENCE`, `MATCHER_TIMEOUT` with the budget and the units used in `details`, or `GEOSPEC_ANALYSIS_LIMIT` with the limit and what the analysis required.

**`GeometryDiagnostic`** — Diagnostic emitted by GeoSpec loaders, analyzers, and matchers.

- **`code`** (`string | (string & {})`, required)
- **`severity`** (`"error" | "warning" | "info"`, required)
- **`message`** (`string`, required)
- **`suggestion`** (`string | undefined`, optional)
- **`spatial`** (`{ min?: Vec3; max?: Vec3; center?: Vec3; } | undefined`, optional)
- **`details`** (`unknown`, optional)

## Errors [#errors]

### GeoSpecAssertionError [#geospecassertionerror]

**TypeScript**

Thrown by every matcher for a failed or refused claim, from `geospec/assertion-client`, with `report`, `claim`, `result`, `diagnostics` and the three canonical byte fields. It has no `code`: read `report.status` for the outcome and `diagnostics[0].code` for the reason.

**Python**

Raised by every matcher for a failed or refused claim, and by `expect_geo` and `load_model` for a request the engine or host rejects. It extends `AssertionError` and adds `code` (the first diagnostic's, or the protocol code), `report`, `result`, `claim`, `diagnostics`, the `canonical_*_bytes` and `protocol_error`, the wrapped native exception.

### GeoSpecModelLoadError [#geospecmodelloaderror]

**TypeScript**

Thrown by `loadModel`, from `geospec/model`, with `diagnostics` as `GeometryDiagnostic` rows; the first one's `code` names the cause.

**Python**

Python has no separate load error: `load_model` raises `GeoSpecAssertionError` for a request it rejects, and lets the engine's `ProtocolError` through for a model over an admission limit.

### ProtocolError [#protocolerror]

Thrown by the engine for a request it does not accept, with `code` and `message`; a refused claim is a result, not an error. `geospec-engine` exports the class and the TypeScript assertions let it through; Python's `geospec_engine_native.ProtocolError` escapes `load_model`, `evaluate_geo` and `query_geo`, and `expect_geo` wraps it in `GeoSpecAssertionError` with the same `code`.

* `invalid-claim`: a negated query, an out-of-range budget, or a subject the engine never admitted. A malformed payload refuses its own claim with `GEOSPEC_INVALID_EXPECTATION` instead.
* `invalid-request`: a closed engine, a WASM engine used before `initialize()`, or a limit the constructor rejects.
* `limit-exceeded`: a load over an admission limit, `maxTriangles` or `maxRetainedSubjects` among them. `capacity` (Python `capacity_code` and `capacity_details`) carries the refusal code, `GEOSPEC_RESOURCE_LIMIT` or `GEOSPEC_ANALYSIS_LIMIT`, with `limitClass`, `unit`, `limit` and `required`.

## Constants [#constants]

* `geoSpecMatcherNames`, from `geospec`: the 26 matcher names in registry order, `toHaveBoundingBox` first and `toSatisfyParallelPlaneDistance` last.
* `geoSpecMaximumWorkUnitLimit`, from `geospec/assertion-client`: 512,000,000, the largest `workUnitLimit` unless a host raises `maximumWorkUnitLimit`.
* Defaults: 8,000,000 work units (`configuration["defaultWorkUnitBudget"]` in Python), a tolerance of 0.02 mm and claim ids `geospec-claim-N`.
* Protocol version 3, registry version 5, canonical profile `geospec-jcs-v1` and 32 capabilities: the 26 matchers, the five queries and `minimumDistance`.
* Profiles: `geospec-entry-config-v1` and `geospec-entry-binary-v1` name the entry limits; an engine constructed with other limits reports `geospec-host-config-v1-<id>` and `geospec-host-binary-v1-<id>`, ids digested from the values.