Reproduce a result
Keep the bytes a result depends on, hash the result bytes the engine wrote, and get the same digest from TypeScript and Python.
The same model bytes, the same claim with its defaults filled in and the same engine profile give the same result bytes, so a result is something you can store, diff and check again later. How it works draws the line between what is reproducible and what is not.
1. Keep what the result depends on
- The model bytes. Every result carries their SHA-256 as
evidence.; the plate's is the SHA-256 of the STEP file itself.subject Content Hash - The claim as the engine ran it. The report keeps the plan with every default filled in,
tolerance: 0.02and a budget of 8,000,000, and the claim id. An automatic id isgeospec-claim-Nin call order, so one chain run twice in one process gets two ids and two digests. - The numeric profile. The result bytes start with the profile that produced them:
{. Byte equality holds within one profile; a new profile is a new baseline."numeric Profile":"geospec- demand- v6", "results":[ …]}
2. Hash the result bytes
Hash the bytes the engine wrote rather than a re-serialized object, so nothing in your own code can reorder them. The report holds them as canonicalResult beside canonicalPlan and canonicalClaim in TypeScript, and as canonical_ beside canonical_plan_bytes and canonical_ in Python.
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { expectGeo } from 'geospec';
import { createGeoSpecAssertionClient } 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' });
const { report } = expectGeo(plate).toHaveBoundingBox({ size: { x: 40, y: 40, z: 20 } });
console.log(report.claimId, createHash('sha256').update(report.canonicalResult).digest('hex'));
const client = createGeoSpecAssertionClient({ engine, claimId: () => 'size' });
const named = client.expectGeo(plate).toHaveBoundingBox({ size: { x: 40, y: 40, z: 20 } });
console.log(named.claimId, createHash('sha256').update(named.canonicalResult).digest('hex'));
await loadModel.dispose();
engine.close();import hashlib
from geospec import GeoSpecEngine, expect_geo, load_model
with GeoSpecEngine(model_root="."):
plate = load_model(file="plate-with-through-bore.step", format="step")
report = expect_geo(plate).to_have_bounding_box(size={"x": 40, "y": 40, "z": 20})
print(report.result["claimId"], hashlib.sha256(report.canonical_result_bytes).hexdigest())
named = expect_geo(plate, claim_id="size").to_have_bounding_box(size={"x": 40, "y": 40, "z": 20})
print(named.result["claimId"], hashlib.sha256(named.canonical_result_bytes).hexdigest())geospec-claim-1 57e6453a170bada55fe5523ee7c452f90af0f48351637fa1f35a6522c28b59a5
size 694611a4ff71d3d77aa3260a5dc5a4a951e331cc7d742d45aa7e1257a68939fbBoth scripts print these two lines, over the native addon and over WASM alike, and print them again on the next run. Run the spec below twice: the history table shows one hash for both runs. Change an expected value and the hash changes with the result; change it back and the first hash returns.
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
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) .toHaveBoundingBox({ size: { x: 40, y: 40, z: 20 } });await loadModel.dispose();engine.close();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.
3. Name the claim
The id enters the bytes, so a stored digest needs a stable one: a claim that always runs at the same position, or a name. Python names a claim per call with claim_id="size". TypeScript names claims through an assertion client: create names every claim size, and the callback receives the matcher name and the sequence number for anything finer. The root expectGeo has no naming option.
Variations
Comparing across versions. A new engine version may carry a new numeric profile, and a measurement fix changes bytes. Store the profile beside the digest, and treat a profile change as a baseline reset rather than a regression.
A refusal is reproducible too. A refused result has bytes like any other: the gear's hole claim from Read a refusal, named hole, hashes to 3b2e504d42b6ae87607020eb6d3350f077b197d56658655fed6ade77a9b2388a in both languages, a record that the question could not be answered on that evidence.
What a matching digest establishes. Two equal digests say that the same engine profile measured the same bytes against the same claim and wrote the same result. They say nothing about what the claim did not ask, and nothing about a model whose bytes differ at all: compare subjectContentHash first.