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

Canonical page: https://www.geospec.xyz/docs/guides/reproduce-a-result

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](https://www.geospec.xyz/docs/how-it-works.mdx#what-a-result-promises) draws the line between what is reproducible and what is not.

### 1. Keep what the result depends on [#1-keep-what-the-result-depends-on]

* **The model bytes.** Every result carries their SHA-256 as `evidence.subjectContentHash`; the plate's is the SHA-256 of the STEP file itself.
* **The claim as the engine ran it.** The report keeps the plan with every default filled in, `tolerance: 0.02` and a budget of 8,000,000, and the claim id. An automatic id is `geospec-claim-N` in 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: `{"numericProfile":"geospec-demand-v6","results":[…]}`. Byte equality holds within one profile; a new profile is a new baseline.

### 2. Hash the result bytes [#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_result_bytes` beside `canonical_plan_bytes` and `canonical_claim_bytes` in Python.

**TypeScript**

```ts title="check.ts"
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();
```

**Python**

```python title="check.py"
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())
```

```console
geospec-claim-1 57e6453a170bada55fe5523ee7c452f90af0f48351637fa1f35a6522c28b59a5
size 694611a4ff71d3d77aa3260a5dc5a4a951e331cc7d742d45aa7e1257a68939fb
```

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

```ts
expectGeo(part)
  .toHaveBoundingBox({ size: { x: 40, y: 40, z: 20 } });
```

### 3. Name the claim [#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: `createGeoSpecAssertionClient({ engine, claimId: () => 'size' })` 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 [#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](https://www.geospec.xyz/docs/guides/read-a-refusal.mdx), 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.