GeoSpec
engine 0.4.0

GeoSpec for agents

The check-and-fix loop an agent runs with GeoSpec, what each verdict demands, and the Markdown endpoints to read.

Open Markdown

GeoSpec gives a coding agent a verdict about a model rather than a picture. The agent writes a spec, one claim per requirement, runs it on the export and reads passed, failed or refused. The claims come from the requirement; every number in a result comes from the model.

1. Run the loop

  1. Model the part.
  2. Export: STEP for feature claims, GLB or STEP for bounds and mass.
  3. Write one claim per requirement, at the drawing's tolerance.
  4. Run the spec; read each verdict.
  5. Fix the model at its root, never the claim. Export and run again.

Never widen a tolerance, drop a claim or remove detail to pass; missing evidence is unsupported, not a pass.

2. Read the three verdicts

VerdictWhat it demands
passedNothing. Keep the canonical result bytes with the model bytes; model, spec and engine version reproduce them (Reproduce a result).
failedThe model differs from the claim. Read what was measured in the diagnostic's details, change the model, re-export.
refusedThe evidence cannot decide the claim; the code names what is missing. Give the claim that evidence, or claim what this subject answers, and report the requirement as unsupported.

A claim for a feature the model lacks fails; a claim is refused only when the evidence cannot decide it, negated or not. Read a refusal maps each code to a change.

3. Match the claim to the evidence

A triangle mesh (GLB or glTF) answers bounds, volume, surface area, mass with a density, centre of mass, watertightness, components and integrity. Faces, holes, patterns, chamfers, fillets, wall thickness, topology and STEP units need a STEP export; on a mesh they refuse with GEOSPEC_UNSUPPORTED_EVIDENCE. The cheat sheet has the full table.

const part = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });

Tab indents. Press Escape, then Tab, to leave the editor.

The spec as a file, with the model loaded over the native addon
check.ts
import { readFile } from 'node:fs/promises';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 (source) => new Uint8Array(await readFile(source as string)) });const part = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });expectGeo(part)  .toHaveCircularHole({ diameter: 12, axis: 'z' });await loadModel.dispose();engine.close();

The engine loads in this tab when you run the spec.

This claim looks for cylindrical faces; the display mesh has none to mark.

plate-with-through-bore.step · 19 KB · copied from an engine test fixture

Run the spec to see what the engine answers for each claim. Then change an expected value and run it again.

 

The drawing calls for a 12 mm bore and the plate carries 10 mm: the claim fails with GEOSPEC_FEATURE_MISMATCH and details.circularHoles lists the hole it found, 10 mm along z. The fix is a 12 mm bore in the CAD source and a new export; the demo cannot re-export, so set diameter to 10 to see the fixed model's answer.

4. Run from a shell

npm install geospec@beta geospec-engine

geospec is the spec API; geospec-engine is the engine, here its native addon (Install covers WASM). A failed or refused claim throws GeoSpecAssertionError; run the script with Node 24 next to the plate:

check.ts
import { readFile } from 'node:fs/promises';
import { expectGeo } from 'geospec';
import { GeoSpecAssertionError } from 'geospec/assertion-client';
import { createModelLoader } from 'geospec/model';
import { Engine } from 'geospec-engine/node';

const engine = new Engine();
const loadModel = createModelLoader({
  engine,
  readSource: async (source) => new Uint8Array(await readFile(source as string)),
});
const plate = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });

try {
  expectGeo(plate).toHaveCircularHole({ diameter: 12, axis: 'z' });
  console.log('passed');
} catch (error) {
  if (!(error instanceof GeoSpecAssertionError)) throw error;
  console.log(error.report.status, error.diagnostics[0].code, error.message);
}
await loadModel.dispose();
engine.close();
$ node check.ts
failed GEOSPEC_FEATURE_MISMATCH No circular hole matches diameter 12 within 0.02 mm.

In a repository, write *.geospec.ts files and run geospec run . from geospec: the exit code is 0 only when every test passed, and a refused claim leaves its test inconclusive (Test parts in CI). Under your own test runner, a failed or refused claim throws GeoSpecAssertionError, so the test fails and a refusal never counts as a pass.

pip install geospec-engine

The wheel installs the geospec module (CPython 3.13 or 3.14) with a pytest plugin; matchers are the registry names in snake case. A failed or refused claim raises GeoSpecAssertionError; evaluate_geo returns the report without raising. Run the script next to the plate:

check.py
from geospec import GeoSpecAssertionError, GeoSpecEngine, expect_geo, load_model

with GeoSpecEngine(model_root="."):
    plate = load_model(file="plate-with-through-bore.step", format="step")
    try:
        expect_geo(plate).to_have_circular_hole(diameter=12, axis="z")
        print("passed")
    except GeoSpecAssertionError as error:
        print(error.result["status"], error.code, error)
$ python check.py
failed GEOSPEC_FEATURE_MISMATCH No circular hole matches diameter 12 within 0.02 mm.

Under pytest the plugin supplies the engine, so a test file needs no fixtures.

5. Read the docs as Markdown

  • /llms.txt: the index, every page linked as Markdown.
  • /llms-full.txt: every page, concatenated.
  • /docs/<slug>.mdx: one page, both language faces labelled, each live demo as its spec fence.

6. Paste into AGENTS.md

Also served at /AGENTS-snippet.md:

AGENTS.md
## GeoSpec geometry checks

- `npm install --save-dev geospec@beta` (Node 24) or `pip install geospec-engine` (CPython 3.13 or 3.14).
- Export z-up: STEP for feature claims, GLB or STEP for bounds and mass; a millimetre GLB declares `sourceUnit: 'mm'` / `source_unit="mm"`.
- Load `loadModel({ source: 'part.step', format: 'step' })` / `load_model(file="part.step", format="step")`; one claim per requirement at the drawing's tolerance (default 0.02 mm).
- Run `geospec run .` over `*.geospec.ts` files (`describe`, `it` and `expectGeo` from `geospec`, `loadModel` from `geospec/model`; exit 0 only when every test passed; a refused claim leaves its test `inconclusive`; `--json` prints the report) or `pytest` over `test_*.py`; each claim is `passed`, `failed` or `refused`.
- Failed means the model is wrong: fix it and re-export; never widen a tolerance, drop a claim or remove detail to pass.
- Refused means the evidence cannot decide: admit the subject the code names or claim what it answers; report it as unsupported, never passed, even negated.
- Keep the canonical result bytes with the model bytes; model, spec and engine version reproduce them.
- Reference: https://www.geospec.xyz/docs/reference/cheat-sheet.mdx and https://www.geospec.xyz/llms.txt

On this page