Cadetta treats the numbers that define a mechanical design as source code. You write every dimension once, as a quantity with units, a tolerance and a citation. Cadetta checks that the design still works for every value in tolerance and drives your CAD files from that one source, so nobody retypes a number. It also tells you exactly what a change invalidates.
Status: 0.1.0 released (Rust core and cadetta CLI, Python binding,
C ABI, FreeCAD and SOLIDWORKS adapters, editor support); the specification
is draft 1.5. The spec is in
docs/spec/.
A CAD model is good at geometry and bad at knowing why a number is what it is. On a real project:
- The same value lives in many places. A bearing's bore drives a shaft, a housing, a spacer and a drawing note. Each file keeps its own copy, and when the vendor changes the part, someone has to find them all.
- Numbers lose their meaning.
12.012mmmight be a press fit, a typo or a measurement. The tolerance, the fit class and the datasheet page it came from are in someone's head or a spreadsheet, not in the model. - Tolerance thinking is done by hand. Will the bolt still go through when both hole patterns are at their limits? Does the stack-up still clear? Engineers work it out on paper, once, and it silently goes out of date.
- Shop knowledge is retyped. Every screw means looking up the clearance hole, the tap drill for that material, the engagement length and the screw length.
- Changes have unknown reach. Swapping a supplier, a material or a motor ripples through checks, sign-offs, analysis scripts and CAD files, and nothing lists which.
- Moving values into CAD is manual. Open, retype or re-import, rebuild, check, repeat. A dimension edited directly in the CAD tool quietly diverges from everything else.
Software solved the same problems with types, compilers, linters, build systems and CI. Cadetta brings those tools to design parameters, and the CAD tool becomes a build target.
Cadetta runs one cycle every time you check or sync a design:
- Write. The design's numbers live in
.cdtfiles: plain text in git, reviewed like code. Each value carries its units, tolerance and source. - Compile. Cadetta evaluates the source into an intermediate representation (IR). The IR is tool-agnostic: every lint, query and adapter reads it, and so can anything that reads JSON.
- Check. The linter and the checks run over the IR for every value in tolerance and every approved alternate. The obligation graph marks which sign-offs, solver results and CAD files are now stale.
- Sync. Adapters, the only part of cadetta that touches a CAD tool, compare the design with what the CAD file holds and with what was last synced. They push the values that changed, rebuild the model, and read the result back.
- Reconcile. Anything that differs on the CAD side becomes drift
you can see. You resolve it by keeping the design or by writing the
CAD-side value back into the source. Existing CAD files enter the
cycle the same way, through
import.
The CAD tool keeps owning shapes and mates. Cadetta owns the numbers, the reasons for them, and the checks on them.
// src/lib.cdt of package acme.rover
use std::fasteners::ScrewJoint
use std::materials::Material
cite vendor = "https://vendor.example/4471-B.pdf"
/// What the rover needs from a caster wheel, whoever makes it.
interface Caster {
hub_bore: Length
hub_width: Length
load_rating >= 400N
}
#[lifecycle(eol, "2027-06")]
ots Vendor4471B: Caster @vendor {
hub_bore = 12mm H7 @vendor:"p.2"
hub_width = 3/4in
load_rating = 450N
unit_cost = 11.20USD
}
ots Colson2Series: Caster @"Colson 2 Series catalog, p.14" {
hub_bore = 12mm H8
hub_width = 7/8in
load_rating = 540N
unit_cost = 13.90USD
}
model Axle(wheel: Caster, span: Length) {
fit seat = p6 on wheel.hub_bore // the shaft side, from ISO 286
end_diameter = seat.shaft
length = 2 * wheel.hub_width + span
}
model Frame {
span = 300mm
material = Material::Al6061T6
datum top = origin.offset(z = 40mm)
datum site = top.offset(x = 60mm)
tapped: [Point2 on site; 4] = rect_pattern(2, 2, 30mm, 20mm) [+-0.05mm]
}
model MotorPlate {
thickness = 5mm
datum base = origin
holes: [Point2 on base; 4] = rect_pattern(2, 2, 30mm, 20mm) [+-0.1mm]
}
assembly Rover {
part wheel[4]: Caster = Vendor4471B alt [Colson2Series]
part frame: Frame
part axle[2]: Axle(wheel = wheel[0], span = frame.span)
part plate: MotorPlate
place frame.origin at origin
place plate.base at frame.site
part plate_screws: ScrewJoint(thread = M4, clamp = plate.thickness, host = frame,
at = plate.holes, onto = frame.tapped)
check load_ok for w in wheel: w.load_rating * 4 >= 1500N
check axle_fits: axle[0].length <= frame.span + 40mm
}
From that, cadetta knows:
- The axle end diameter is the p6 shaft for the wheel's actual bore. If the wheel changes, so does the shaft. No number was copied.
- The motor plate's four screws:
- clearance holes, the tap drill for 6061, thread and drill depth, screw length and the drawing callout are all computed and cited;
- both hole patterns line up at every tolerance limit;
- the screws are in the bill of materials.
load_okandaxle_fitshold for the primary wheel, and are checked again for each approved alternate.- The wheel's end-of-life date is tracked: after June 2027, using it without a viable alternate is an error.
When something does not hold, the message says why, where and for which variant:
warning[I002]: approved alternate is not viable
--> cad/rover.cdt:59:3
|
59 | check axle_fits: axle[0].length <= frame.span + 40mm
| ^^^^^^^^^^^^^^^ violated under wheel=Colson2Series
|
= note: axle[0].length = 344.45mm (2 * 7/8in + 300mm); limit 340mm
= note: holds for the primary: 338.1mm
= help: the end-of-life wheel has no viable alternate after 2027-06 (L001)
= path: acme.rover::Rover.axle_fits
The swap you were planning for the end-of-life wheel would not have
fit, and you learn that before anyone orders a part. The same check
failing for the primary would be error[C001] and would fail the
build.
A language for design intent.
- Quantities are first-class:
5in,3/4in,12mm H7,10mm [+-0.05mm]. Values are exact rationals, so converting inches to millimetres and back loses nothing. - Units and kinds are checked: a torque is never assigned to an energy.
- References replace copies,
letholds stack-ups, and tables hold catalog data. - Every value can cite its source: a document, a page,
@measuredor@assumed.
Tolerance-aware checks.
checkandrequireare three-valued: holds, violates, or undecided over the tolerance range.- Stack-ups are exact affine forms, so a dimension that appears twice is counted once.
- ISO 286 fits, including one-sided fits such as
H7 on motor.shaft.
Positions without a geometry kernel.
- Points are typed by datum the way lengths are typed by unit.
- Parts are placed in a tree, so a second bolted connection is a check rather than a solve.
- Hole patterns, alignment, coaxiality and bolt clearance float are checked under tolerance.
Shop knowledge as a library.
ScrewJointcovers clearance holes, tap drills by material, engagement, screw length, counterbores and callouts.- Fits, materials and retaining rings are included.
- The numbers a shop disagrees about live in an overridable, cited policy. Metric is in 0.1.0; inch and inserts follow in 0.2.0.
Swappable parts.
- Designs depend on interfaces, not SKUs.
- Every approved alternate is evaluated continuously.
impact --swappreviews a swap before you make it: the values and verdicts that change, the CAD files to re-sync, the sign-offs invalidated, and the cost, lead-time and mass deltas.swapapplies it.
An obligation graph.
- Every check, fit, sign-off, citation, solver result and CAD file is pinned to Merkle digests of exactly what it read.
impact --since HEADlists what your change invalidates before you commit.why --stalenames the upstream edit that made a sign-off stale.- Role-based sign-offs live in a mergeable
cadetta.lock.
Packages.
- Shared shop policies and parts libraries are packages, depended on by version; a product line is a workspace.
- Modules come from the file tree, and a library's API is its
pubitems (pub usere-exports a curated surface). - Dependencies are pinned in
cadetta.lockby digest, andcadetta updatere-resolves them. A library update shows up in the obligation graph as exactly the checks and sign-offs it makes stale.
External analysis.
- Values from a Python or MATLAB script come in through a small JSON protocol.
- Results are cached by input digest, checked for kind, and marked stale when any input changes.
CAD integration.
- SOLIDWORKS is the primary target: read, push, rebuild and read back through COM.
syncis a three-way merge. An edit made in SOLIDWORKS is reported, never silently overwritten, andimport --updatewrites it back into the source.importadopts an existing part.- A zero-install path writes a SOLIDWORKS linked-equations file.
- FreeCAD is the CI-testable reference adapter; JSON and CSV emitters are built in.
Python and everything else.
import cadettagives exact quantities and every CLI query from Python, plus decorators for writing solvers and adapters.- Any other language uses the JSON outputs and protocols.
| Stage | You do | Cadetta does |
|---|---|---|
| Adopt | cadetta import solidworks Bracket.SLDPRT, review the proposal, --accept |
Turns globals, equations and dimensions into named, bound .cdt source |
| Design | Edit .cdt; reference, don't copy; declare fits, joints and checks |
Computes derived values; checks every value in tolerance and every alternate |
| Build | cadetta sync |
Pushes changed values into SOLIDWORKS, rebuilds, reads back, and reports drift and rebuild errors |
| Analyse | Write a solver in Python or MATLAB | Runs it, caches the result, and makes dependent checks stale when inputs change |
| Review | cadetta ack check:**/seat* --role tolerance_review |
Records sign-off against the exact digests reviewed |
| Change | cadetta impact --since HEAD before committing |
Lists changed values, flipped verdicts, stale sign-offs and CAD files to re-sync |
| Re-source | cadetta impact --swap acme.rover::Rover/wheel acme.rover::Colson2Series, then swap |
Previews and applies the swap, with commercial deltas |
| Update | cadetta update acme.shop, then cadetta impact --worktree |
Re-resolves a dependency, rewrites the lock, and lists the verdicts and sign-offs it flips |
| Release | cadetta check --release in CI |
Fails on anything violated, undecided past policy, stale or unsigned |
Cadetta is not a CAD tool and not a geometry kernel. It does not model solids, solve mates, find interference between shapes, or do GD&T tolerance zones. It is also not a PDM system. It owns the numbers, the reasons for them and the checks on them, and leaves geometry to the CAD tool.
The core is Rust. The cadetta command ships on crates.io and on PyPI;
the PyPI package is also importable (import cadetta):
cargo install cadetta # or
pip install cadetta # or: uv tool install cadettaAdapters are separate: cadetta-freecad ships on PyPI; the SOLIDWORKS
adapter (C#, adapters/solidworks) and the Fusion adapter
(adapters/fusion) are built from this repository. A C ABI
(libcadetta_capi, cadetta.h) ships as release archives for C, C++,
MATLAB and Julia (see bindings/README.md).
cadetta init --adapter solidworks # cadetta.toml, src/lib.cdt
cadetta import solidworks Bracket.SLDPRT # propose names and bindings
cadetta import --accept # write bindings and .cdt
cadetta check # compile, lint, check
cadetta update # re-resolve dependencies, write the lock
cadetta sync --dry-run # what a sync would change
cadetta sync # push, rebuild, read back
cadetta why acme.rover::Rover/axle[0].end_diameterSyntax highlighting and cadetta lsp (diagnostics, formatting, hover, code lens, go to definition) for VS Code, Sublime Text, Vim/Neovim, Emacs and JetBrains: see editors/README.md.
| Document | Contents |
|---|---|
| Specification | Index, scope, open questions |
| Quantities | Units, kinds, tolerances, exactness |
| Language | .cdt syntax and semantics |
| Positions and datums | Points, placements, alignment |
| Obligations | Digests, sign-offs, impact and swap queries, the lock file |
| Adapters | Sync, import, the adapter protocol, CAD targets |
| Standard library | Fits, materials, fasteners, ScrewJoint |
| Lints | Every diagnostic code |
| Config and CLI | cadetta.toml and every command |
| Bindings | Python and the JSON catch-all |
| Decisions | Why things are the way they are |
| Golden corpus | Realistic designs with hand-evaluated expected results |
| History | How the spec evolved |
| Path | Contents |
|---|---|
docs/spec/ |
The specification |
docs/decisions/ |
Architecture decision records |
docs/schemas/ |
Generated JSON schemas of the IR and the CLI, solver and adapter documents |
tests/golden/ |
Golden corpus: realistic designs with hand-evaluated expected results |
docs/history/ |
How the spec evolved: pilot rounds and review reports |
crates/ |
The Rust workspace: units, syntax, sema, fits, geometry, ir, lint, obligations, solve, variants, adapter, log, and the cadetta binary (cadetta-cli) |
std/ |
The standard library package (std::fits, std::materials, std::fasteners, std::inserts, ...), embedded in the binary |
bindings/ |
Language bindings, one directory per language (bindings/python is the importable cadetta, bindings/c the C ABI); other languages use the JSON interfaces |
adapters/ |
External CAD adapters: freecad (the reference adapter), solidworks (C#, the primary target), fusion |
editors/ |
Syntax highlighting and cadetta lsp setup for VS Code, Sublime Text, Vim/Neovim, Emacs, JetBrains, tree-sitter |
design/ |
The grimble architecture model |
tickets/ |
The frob ticket ledger (do not edit by hand) |
See CONTRIBUTING.md and SECURITY.md. MIT licensed; see LICENSE.