partspec
Verify CAD-as-code parts against declared engineering intent.
Status: pre-alpha; v0.1.0 is on PyPI. It runs end to end —
checkandmeasureacross all three engines,renderon the mesh tier — and is dogfooded on real parts. The check vocabulary is deliberately small;docs/POST-V0.mdrecords what is withheld and why. Expect the Python API to move: the stable surface is the report schema plus the exit codes, andpartspec.run()is internal.
What it is for
You already write down what a part has to be true of — a minimum wall, a bolt circle, a
bore that has to clear an 8 mm shaft. Usually it lives in a README, a comment, or your
head, and nothing checks it. partspec lets you declare it next to the model and enforce
it in CI.
This is examples/spacer/spec.py — the whole contract, less its
docstring:
from partspec import Part, openscad
PLATE = (40.0, 30.0, 6.0)
BORE_D = 8.0
WALL_MIN = 2.0
def spacer() -> Part:
p = Part("example-spacer", openscad(
"spacer.scad",
plate_x=PLATE[0], plate_y=PLATE[1], plate_z=PLATE[2],
bore_d=BORE_D, wall=WALL_MIN,
))
# Parameter phase — arithmetic over the inputs, no engine needed.
p.requires("bore_d + 2 * wall <= plate_y")
p.requires("bore_d > 0")
p.param("plate_z", min=1.0)
# Geometry phase.
p.envelope(max=PLATE)
p.watertight()
p.solid_count(1)
p.genus(1) # one bore straight through
return p
$ partspec check examples/spacer/spec.py:spacer
ok bore_d_2_wall_le_plate_y
ok bore_d_gt_0
ok param:plate_z
ok builds
ok envelope
ok watertight
ok solid_count
ok genus
PASS: 8 pass
/home/user/partspec/examples/spacer/outputs/spec-spacer/report.json
The JSON report is the actual product surface; the console summary is a courtesy. Exit
codes: 0 pass, 1 fail, 2 incomplete, 3 empty, 4 error, 64 bad usage.
Writing a contract for a part you did not model? partspec measure dumps every quantity
the backend can honestly produce, with no verdict — so you can see the numbers before
deciding which of them are intent. It will not write the checks for you: a check the tool
wrote is a check nobody decided.
If the author is an AI agent, partspec is the gate at the end of its loop. The
authoring session owns making the part; partspec proves the result against intent the
model does not contain, and persists the proof — that boundary is
D18. The mcp
extra puts the gate in the agent's tool list: check returns the same report the CLI
writes, measure and render the same output as their verbs, every call a fresh stateless
evaluation. And the loop is measured, not assumed: in the seeded-defect eval suite
(evals/), an agent shown
only the report — no shell, no hints, contract frozen — repaired all five defect classes in
a single edit each, without once weakening its contract.
The idea it is built around
A verification tool that reports a green result it has not earned is worse than no tool,
because it converts an open question into a false assurance. So partspec has five check
statuses and only one of them is green:
| status | meaning |
|---|---|
pass |
evaluated and satisfied, conclusively |
fail |
evaluated and violated, conclusively |
approximate |
evaluated, but the error interval straddles the limit — indeterminate |
unsupported |
this backend cannot evaluate this check on this geometry at all |
skipped |
not evaluated |
A part whose checks were mostly unavailable exits 2, not 0. A contract that asserts
nothing exits 3. Neither is a pass, because neither established anything.
This matters most across engines. OpenSCAD emits a triangle mesh, which has no cylindrical
faces — so a hole diameter is genuinely unanswerable there, and partspec says so instead
of fitting a circle to the facets and reporting a confident wrong number. (An OpenSCAD
cylinder($fn=16) is a real 16-sided prism: fitting recovers Ø10.000 for a bore that
actually clears Ø9.808, and the error is always in the unsafe direction.)
It also matters on broken output. A CAD engine will happily exit 0 having written a mesh
that is open or non-manifold, and most measurement libraries will then hand you a volume
for it — a number that is not a bad estimate, but not a volume at all. Every measurement
here states its precondition and refuses when it fails, naming the defect:
n/a volume — volume is the integral over a closed surface; this mesh has 4 non-manifold edge(s)
Refusal is kept as narrow as the mathematics allows. An open mesh still determines its own
body count, so solid_count still answers there; only a non-manifold junction, where
counting through and counting across disagree, makes it refuse. An unnecessary
unsupported is its own way of failing to answer an answerable question.
Engines
| engine | tier | notes |
|---|---|---|
| OpenSCAD | mesh | via binary STL, measured with trimesh |
| build123d | OCCT | native |
| CadQuery | OCCT | adopted into the build123d backend via .wrapped — same kernel, no conversion |
One contract, evaluated identically wherever it can be, with honest degradation where it cannot.
Install
pip install 'partspec[mesh]' # OpenSCAD parts — the smallest useful install
Or for development, from a clone:
uv sync --all-extras # or: just setup
uv run partspec check examples/spacer/spec.py:spacer
Engines are optional extras — mesh, occt, cadquery — so uv sync --extra mesh is
enough for OpenSCAD-only work. The mcp extra adds partspec-mcp, a stdio MCP server
exposing check, measure and render as stateless tools: each call runs the CLI in a
fresh subprocess and returns its artifact, per the boundary in D18. The openscad binary itself is a system dependency;
PARTSPEC_OPENSCAD pins which one is used, and the version is recorded in every report
because it changes the artifact.
Installing both Python engines with plain pip needs one extra step:
pip install 'partspec[occt,cadquery]'
pip install --force-reinstall --no-deps cadquery-ocp # re-assert the VTK build
build123d wants cadquery-ocp-novtk and CadQuery wants cadquery-ocp. Both wheels install
the same top-level OCP/ package, neither pip nor uv detects the conflict, and whichever
lands last wins — when novtk wins, CadQuery cannot import at all. This repo drops novtk with
a [tool.uv] override, but that is a workspace setting and is not carried in wheel
metadata, so a pip install has no override in scope. If you skip the second line, partspec
tells you so: the clobber is reported as an environment fault with that command as the hint,
not as a failing part.
Documentation
The specs are normative and were written before the implementation:
docs/SPEC-report.md— the report schema and exit codes. This is the actual contract; the CLI verbs are not.docs/SPEC-contract.md— the Python contract API and check vocabulary.docs/SPEC-backend.md— the geometry backend protocol.docs/DECISIONS.md— every design decision, with its reasoning.docs/PLAN.md— what v0 is and how it gets built.docs/POST-V0.md— what is deliberately not here yet, and why.
Prior art
partspec owes its assertion model to cad-khana
(Apache-2.0), which arrived at declaring claims alongside the model, tri-state results, and
a diagnostics-first CLI independently and first.
PartCAD is the reference for engine-neutral part
packaging, and its -D parameter-passing approach is adopted directly.
build123d-mcp is the complement on the authoring
side — a stateful interactive session an agent designs in; partspec is the stateless
gate the result must pass, and deliberately does not own that loop
(D18).
License
Apache-2.0.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file partspec-0.1.0.tar.gz.
File metadata
- Download URL: partspec-0.1.0.tar.gz
- Upload date:
- Size: 352.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7011c5e9e8a6a77b884a1b7bb877f3c67aae35ad5a337f39483b8841cb873345
|
|
| MD5 |
c53fa944cb45c85c140a849aae67fb73
|
|
| BLAKE2b-256 |
90a923dcd66f8734921ce1c5c604fcde420a78503c2ee54179211a999909f09f
|
Provenance
The following attestation bundles were made for partspec-0.1.0.tar.gz:
Publisher:
release.yml on CameronBrooks11/partspec
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
partspec-0.1.0.tar.gz -
Subject digest:
7011c5e9e8a6a77b884a1b7bb877f3c67aae35ad5a337f39483b8841cb873345 - Sigstore transparency entry: 2373290330
- Sigstore integration time:
-
Permalink:
CameronBrooks11/partspec@a6f683caff73961923b7f5fd9c6c55fdc2439300 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/CameronBrooks11
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a6f683caff73961923b7f5fd9c6c55fdc2439300 -
Trigger Event:
push
-
Statement type:
File details
Details for the file partspec-0.1.0-py3-none-any.whl.
File metadata
- Download URL: partspec-0.1.0-py3-none-any.whl
- Upload date:
- Size: 76.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79a8f970d69cb5e795caeb54bac0088e3dc8b67e591c4bea71d5ab746d05c76a
|
|
| MD5 |
17c77843fcdb5e79384cb00fb3954c6f
|
|
| BLAKE2b-256 |
faef4d1f0c1c5b4e8dcc4f82d4f0a04015accf8d784b9c2bded03fad3509e928
|
Provenance
The following attestation bundles were made for partspec-0.1.0-py3-none-any.whl:
Publisher:
release.yml on CameronBrooks11/partspec
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
partspec-0.1.0-py3-none-any.whl -
Subject digest:
79a8f970d69cb5e795caeb54bac0088e3dc8b67e591c4bea71d5ab746d05c76a - Sigstore transparency entry: 2373290615
- Sigstore integration time:
-
Permalink:
CameronBrooks11/partspec@a6f683caff73961923b7f5fd9c6c55fdc2439300 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/CameronBrooks11
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a6f683caff73961923b7f5fd9c6c55fdc2439300 -
Trigger Event:
push
-
Statement type: