# Write a spec from a drawing

Map each callout on a part drawing to a matcher, take the tolerance from the tolerance block and run the spec against the export.

Canonical page: https://www.geospec.xyz/docs/agents/spec-from-a-drawing

A drawing says what a part must be. A spec says the same as claims the engine can decide: one callout, one matcher, one expectation. The claims come from the drawing; the measurements come from the model.

## Map the callouts [#map-the-callouts]

| Callout                     | Claim                                                                                         |
| --------------------------- | --------------------------------------------------------------------------------------------- |
| Overall 40 × 40 × 20        | `toHaveBoundingBox({ size: { x: 40, y: 40, z: 20 } })`                                        |
| Ø10 on centre, along z      | `toHaveCircularHole({ diameter: 10, axis: 'z', center: { x: 0, y: 0 } })`                     |
| 6 × Ø5 on a Ø45 bolt circle | `toHaveCircularHolePattern({ count: 6, holeDiameter: 5, boltCircleDiameter: 45, axis: 'z' })` |
| 1 × 45° chamfer             | `toHaveChamferFeature({ distance: 1 })`                                                       |
| Fillet radius 2             | `toHaveFilletFeature({ radius: 2 })`                                                          |
| Wall 2 min                  | `toHaveMinimumWallThickness({ value: { greaterThanOrEqual: 2 } })`                            |
| Volume                      | `toHaveVolume({ value: 30429.2 })`                                                            |
| Mass, aluminium 2.7 g/cm³   | `toHaveMass({ value: 82.16, density: 0.0027 })`                                               |

Python spells each matcher in snake case with the same keys as keyword arguments: `to_have_circular_hole_pattern(count=6, holeDiameter=5, boltCircleDiameter=45, axis="z")`. Every feature row needs a STEP export; three rows need care:

* **Holes.** `toHaveCircularHole` decides diameter, axis, centre and `through`; it has no depth field, so a depth callout stays a requirement the spec reports as unsupported.
* **Wall thickness.** The engine measures the exact minimum of a complete box or a right cylinder only ([admitted geometry](https://www.geospec.xyz/docs/reference/features.mdx#tohaveminimumwallthickness)); a bored plate is neither, so the claim refuses with `GEOSPEC_UNSUPPORTED_EVIDENCE`. Keep the callout and report it as unsupported.
* **Mass.** `density` in g/mm³ gives grams. Without it the claim refuses on every subject.

## Take the tolerance from the drawing [#take-the-tolerance-from-the-drawing]

A tolerance block of ±0.1 becomes `tolerance: 0.1` on the claims it covers; a claim without one is held to 0.02 mm ([Set tolerances and budgets](https://www.geospec.xyz/docs/guides/set-tolerances-and-budgets.mdx)). A limit such as "2 min" is a strict comparison, `greaterThanOrEqual`, with no tolerance. The tolerance never moves to make a claim pass: a failure at the drawing's tolerance is a model that misses the drawing.

## The plate [#the-plate]

The drawing in words: a 40 × 40 × 20 mm aluminium plate, origin at the centre of its bottom face, Ø10 bore on centre along z. Its volume is 40 × 40 × 20 less π × 5² × 20, or 30429.2 mm³; at 0.0027 g/mm³ that is 82.16 g, centre of mass `[0, 0, 10]`.

```ts
expectGeo(part)
  .toHaveBoundingBox({ size: { x: 40, y: 40, z: 20 } });
expectGeo(part)
  .toHaveCircularHole({ diameter: 10, axis: 'z', center: { x: 0, y: 0 } });
expectGeo(part)
  .toHaveVolume({ value: 30429.2 });
expectGeo(part)
  .toHaveMass({ value: 82.16, density: 0.0027 });
expectGeo(part)
  .toHaveCenterOfMass({ point: [0, 0, 10] });
```

All five pass. Set `x` to 41, or `diameter` to 12, and that claim fails with the measured value in its diagnostic; the drawing's number then comes from the model.

As a file, give each callout its own test: a failed or refused claim ends the test it is in, so one test per callout keeps every verdict visible. [GeoSpec for agents](https://www.geospec.xyz/docs/agents/for-agents.mdx#4-run-from-a-shell) runs a spec from a shell.

## Variations [#variations]

**A mesh export.** On a GLB of the part the size, volume, mass and centre rows still answer; the bore row refuses with `GEOSPEC_UNSUPPORTED_EVIDENCE`. [Read a refusal](https://www.geospec.xyz/docs/guides/read-a-refusal.mdx) says what to change.

**A range on the drawing.** "30400 to 30500 mm³" is `toHaveVolume({ value: { greaterThan: 30400, lessThan: 30500 } })`, in Python `to_have_volume(value={"greaterThan": 30400, "lessThan": 30500})`; the comparisons are strict and carry no tolerance.

**A part with no drawing.** Query [`analyzeBrep`](https://www.geospec.xyz/docs/reference/queries.mdx#analyzebrep) to list the holes, patterns, chamfers and fillets the model carries, then write claims from the requirement, not the inventory. A claim copied from the measurement passes on every model, including a wrong one.