{"_id":"@axle-keys/cad-engine","_rev":"7-8f4f6b5bf009b24db0f2d55feab21781","name":"@axle-keys/cad-engine","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@axle-keys/cad-engine","version":"0.1.0","keywords":["cad","parametric","replicad","opencascade","step","stl","geometry","agent","mcp"],"license":"MIT","_id":"@axle-keys/cad-engine@0.1.0","maintainers":[{"name":"hkhenning","email":"hkrishenning@gmail.com"}],"homepage":"https://axlekeys.com","bugs":{"url":"https://github.com/AxleKeys/cad-engine/issues"},"bin":{"axle":"packages/engine/cli.mjs"},"dist":{"shasum":"e1598a472fd15f5c1b8beed9fa9ad934b3c347e9","tarball":"https://registry.npmjs.org/@axle-keys/cad-engine/-/cad-engine-0.1.0.tgz","fileCount":50,"integrity":"sha512-3o4X61JYWXdVunJg0ZNRWnuA7aQdSqFKHG5IsqKcc10E9xtFyjSlL+zurCXRSRfV1agsH8Jw1Uq50/eyUGUQRA==","signatures":[{"sig":"MEUCIFxC+BEjWaq3wRDAdJcCV+iD0B3ScHH8DXgT2TeBF+hqAiEAh2HDQkWS5f/H8/T2V8juL7dAEgUNJ/+6qAaNGR4X/fQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":593906},"type":"module","engines":{"node":">=23"},"gitHead":"46c98092ee26f7a5f3aa84fbe64299cdb0f1c27d","scripts":{"test":"node scripts/verify.mjs"},"_npmUser":{"name":"hkhenning","email":"hkrishenning@gmail.com"},"repository":{"url":"git+https://github.com/AxleKeys/cad-engine.git","type":"git"},"_npmVersion":"11.9.0","description":"Run Axle Keys model code locally: build parametric CAD, get the same verdict axlekeys.com gives, export STEP/STL. Offline, ~55ms per iteration.","directories":{},"_nodeVersion":"24.14.0","dependencies":{"replicad":"0.23.0","replicad-opencascadejs":"0.23.0"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cad-engine_0.1.0_1788628076391_0.6778434026989233","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@axle-keys/cad-engine","version":"0.1.1","keywords":["cad","parametric","replicad","opencascade","step","stl","geometry","agent","mcp"],"license":"MIT","_id":"@axle-keys/cad-engine@0.1.1","maintainers":[{"name":"hkhenning","email":"hkrishenning@gmail.com"}],"homepage":"https://axlekeys.com","bugs":{"url":"https://github.com/AxleKeys/cad-engine/issues"},"bin":{"axle":"packages/engine/cli.mjs"},"dist":{"shasum":"c9c8c310a69291626afbb7abca9845dbed447ec3","tarball":"https://registry.npmjs.org/@axle-keys/cad-engine/-/cad-engine-0.1.1.tgz","fileCount":50,"integrity":"sha512-iaVHnhse06ZwBuZSxDpZuomJ8FuvVa3Rv0RDUaGVnVzM4JskiMDlPaY7xIAe9Qr5awL6EN+soxsu1MMV7HCCWA==","signatures":[{"sig":"MEQCIFSDAtVV+3p3ZKTppyeCJsxv6obQn23Bp8fRagT27YQHAiAQcFO1R/zMeSugS7pOXrpLOY2kU+i0VqP8v3B5dxhitw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":594449},"type":"module","engines":{"node":">=23"},"gitHead":"274adef365dd331d2fabbf9def69cda851275db7","scripts":{"test":"node scripts/verify.mjs"},"_npmUser":{"name":"hkhenning","email":"hkrishenning@gmail.com"},"repository":{"url":"git+https://github.com/AxleKeys/cad-engine.git","type":"git"},"_npmVersion":"11.9.0","description":"Run Axle Keys model code locally: build parametric CAD, get the same verdict axlekeys.com gives, export STEP/STL. Offline, ~55ms per iteration.","directories":{},"_nodeVersion":"24.14.0","dependencies":{"replicad":"0.23.0","replicad-opencascadejs":"0.23.0"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cad-engine_0.1.1_1788628586266_0.06966570470499445","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@axle-keys/cad-engine","version":"0.1.2","keywords":["cad","parametric","replicad","opencascade","step","stl","geometry","agent","mcp"],"license":"MIT","_id":"@axle-keys/cad-engine@0.1.2","maintainers":[{"name":"hkhenning","email":"hkrishenning@gmail.com"}],"homepage":"https://axlekeys.com","bugs":{"url":"https://github.com/AxleKeys/cad-engine/issues"},"bin":{"axle":"packages/engine/cli.mjs"},"dist":{"shasum":"71a146b9b5a8f93d5263d5a5e2d5326162bd6598","tarball":"https://registry.npmjs.org/@axle-keys/cad-engine/-/cad-engine-0.1.2.tgz","fileCount":56,"integrity":"sha512-0srL5StZyDv/sHh+9K6qnHjEZ5a3oFjFvvy/Zz6lWXjnwLHX0+lGG4HEQsuQI9CSbhl/+FOSW+s74KFAd2HhsQ==","signatures":[{"sig":"MEUCIF1HG4rqzg9Qqv32JqfjwBCYzjZbqeHD1KwP672qhFoeAiEA5oAQHeEVUv+xV1DbcwCdO7XRQiriHvM2b1c84c625aw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1568392},"type":"module","engines":{"node":">=23"},"gitHead":"8496557e3817c96c9b8bf36f37f9ab9365d6cbd6","scripts":{"test":"node scripts/verify.mjs"},"_npmUser":{"name":"hkhenning","email":"hkrishenning@gmail.com"},"repository":{"url":"git+https://github.com/AxleKeys/cad-engine.git","type":"git"},"_npmVersion":"11.9.0","description":"Run Axle Keys model code locally: build parametric CAD, get the same verdict axlekeys.com gives, export STEP/STL. Offline, ~55ms per iteration.","directories":{},"_nodeVersion":"24.14.0","dependencies":{"replicad":"0.23.0","replicad-opencascadejs":"0.23.0"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cad-engine_0.1.2_1788733819791_0.5654104348053368","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@axle-keys/cad-engine","version":"0.1.3","keywords":["cad","parametric","replicad","opencascade","step","stl","geometry","agent","mcp"],"license":"MIT","_id":"@axle-keys/cad-engine@0.1.3","maintainers":[{"name":"hkhenning","email":"hkrishenning@gmail.com"}],"homepage":"https://axlekeys.com","bugs":{"url":"https://github.com/AxleKeys/cad-engine/issues"},"bin":{"axle":"packages/engine/cli.mjs"},"dist":{"shasum":"775b8ba382f47221493e6803e2c40a368ce0bbc7","tarball":"https://registry.npmjs.org/@axle-keys/cad-engine/-/cad-engine-0.1.3.tgz","fileCount":56,"integrity":"sha512-inkNOmR00I07wVvnymZSr9x1g7IRyNn9rVUFMeJraZQG5A00l4yVl06RMa3Ld2NdlvQzEif6eFP9/Sgvqccfpg==","signatures":[{"sig":"MEUCIQC9oI/CfQMlQzYbvYYmMYuPrZnfsosok7pYWSnoz97+ggIgPreVl4zDPsR6TLwemqWV2hp1fx7cU+hhHN65cSxX46U=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1561403},"type":"module","engines":{"node":">=23"},"gitHead":"0027493ed4d95bd0a144df3f7239024783109683","scripts":{"test":"node scripts/verify.mjs"},"_npmUser":{"name":"hkhenning","email":"hkrishenning@gmail.com"},"repository":{"url":"git+https://github.com/AxleKeys/cad-engine.git","type":"git"},"_npmVersion":"11.9.0","description":"Run Axle Keys model code locally: build parametric CAD, get the same verdict axlekeys.com gives, export STEP/STL. Offline, ~55ms per iteration.","directories":{},"_nodeVersion":"24.14.0","dependencies":{"replicad":"0.23.0","replicad-opencascadejs":"0.23.0"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cad-engine_0.1.3_1788739030491_0.5534735090198091","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@axle-keys/cad-engine","version":"0.1.4","keywords":["cad","parametric","replicad","opencascade","step","stl","geometry","agent","mcp"],"license":"MIT","_id":"@axle-keys/cad-engine@0.1.4","maintainers":[{"name":"hkhenning","email":"hkrishenning@gmail.com"}],"homepage":"https://axlekeys.com","bugs":{"url":"https://github.com/AxleKeys/cad-engine/issues"},"bin":{"axle":"packages/engine/cli.mjs"},"dist":{"shasum":"c6c3742edfc10bf324928ccd6388ada8d08d000a","tarball":"https://registry.npmjs.org/@axle-keys/cad-engine/-/cad-engine-0.1.4.tgz","fileCount":58,"integrity":"sha512-z49g/YtSmkg2dfKB04GjXOguXf5ZWCY5r95J3xc51hKqWdqwa7OcBcQBx0BQhWdah+9HWjIqh0oWIwOFIarscQ==","signatures":[{"sig":"MEUCIBLwqGuP2AnMaQdlwyqeFBkrrqVOct4wklkDgz+fBIqwAiEAinzI7y8JFJUzfjufR00AexG5LkCqkr/XcB2Lb0VwpdI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1656831},"type":"module","engines":{"node":">=23"},"gitHead":"7396a7c4391e8fa4de6cf144945b1e80a660c6fe","scripts":{"test":"node scripts/verify.mjs"},"_npmUser":{"name":"hkhenning","email":"hkrishenning@gmail.com"},"repository":{"url":"git+https://github.com/AxleKeys/cad-engine.git","type":"git"},"_npmVersion":"11.9.0","description":"Run Axle Keys model code locally: build parametric CAD, get the same verdict axlekeys.com gives, export STEP/STL. Offline; warm rebuilds in tens of milliseconds.","directories":{},"_nodeVersion":"24.14.0","dependencies":{"replicad":"1.0.1","replicad-opencascadejs":"1.0.0"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cad-engine_0.1.4_1789259920192_0.09042442743823065","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"name":"@axle-keys/cad-engine","version":"0.1.5","keywords":["cad","parametric","replicad","opencascade","step","stl","geometry","agent","mcp"],"license":"MIT","_id":"@axle-keys/cad-engine@0.1.5","maintainers":[{"name":"hkhenning","email":"hkrishenning@gmail.com"}],"homepage":"https://axlekeys.com","bugs":{"url":"https://github.com/AxleKeys/cad-engine/issues"},"bin":{"axle":"packages/engine/cli.mjs"},"dist":{"shasum":"6278f585d70941eaf94ef7f9ba1bc9fa5e92cd82","tarball":"https://registry.npmjs.org/@axle-keys/cad-engine/-/cad-engine-0.1.5.tgz","fileCount":59,"integrity":"sha512-EtuCH2OeGqLa1UhGpSDvU2ToaEay6QqksqquSfumEY2jHCuDQ06rCnOwb/Cd5BSoYwSIRunYEChmxO1xmi5ksg==","signatures":[{"sig":"MEUCIQDZruHekPJZ28OpKR8bmPtuDZ0wlfYUHt7mv7eSKPP8xgIgYqbJ8VZD5NGf9IPNBG8JTY2Co/LNJFnCavoa+034wWE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCVX7NhbRgRqqqtakv7Odqg/+SGWDss8zYz0TXUH+n2VQIgSPxHVuBCKiUheSjoKXgc+6lNXz5mWKKc8JsIYavaU/o=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1667346},"type":"module","engines":{"node":">=23"},"gitHead":"0b4ad830454d410d03ff08bb1e7820f06c0c96ed","scripts":{"test":"node scripts/verify.mjs"},"_npmUser":{"name":"hkhenning","email":"hkrishenning@gmail.com"},"repository":{"url":"git+https://github.com/AxleKeys/cad-engine.git","type":"git"},"_npmVersion":"11.9.0","description":"Run Axle Keys model code locally: build parametric CAD, get the same verdict axlekeys.com gives, export STEP/STL. Offline; warm rebuilds in tens of milliseconds.","directories":{},"_nodeVersion":"24.14.0","dependencies":{"replicad":"1.0.1","replicad-opencascadejs":"1.0.0"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cad-engine_0.1.5_1789482466096_0.8431516057914077","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"_id":"@axle-keys/cad-engine@0.2.0","bin":{"axle":"packages/engine/cli.mjs"},"bugs":{"url":"https://github.com/AxleKeys/cad-engine/issues"},"dist":{"shasum":"12ed0d95e0eb4501ec599b9936b16b8fd3c3a4d0","tarball":"https://registry.npmjs.org/@axle-keys/cad-engine/-/cad-engine-0.2.0.tgz","fileCount":68,"integrity":"sha512-pOoH60Rc7N8XNtNt6AKopEeAv1VlV79wGGi0yqyFIX12krnOzbzWxRtra2qeKEdp0AQNNwzJsFs0A/Kg1RrJ2w==","signatures":[{"sig":"MEQCIGN9mC4EnCYlkMMDcj+CLJx7m43392wKeu6y7ZAW31mhAiB9/sKoiDhYDh7BYx5RNR1vyZSQioG/aHnArEfZGUnXpw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCHjI9Kqko1T23qneVlWsXWLTHPL7+zdoHfBg6wmcADyQIhAKmzu9ESFHu1VfguTwMeiw0rNU5YOz8vJrWrCsIQ9ry9"}],"unpackedSize":1853004},"name":"@axle-keys/cad-engine","type":"module","engines":{"node":">=23"},"gitHead":"7166bb26aeb106a7efe4d0e9891b89af6325e7eb","license":"MIT","scripts":{"test":"node scripts/verify.mjs"},"version":"0.2.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d2218373-2f8d-4647-9f96-d7e302d1f829"}},"homepage":"https://axlekeys.com","keywords":["cad","parametric","replicad","opencascade","step","stl","geometry","agent","mcp"],"repository":{"url":"git+https://github.com/AxleKeys/cad-engine.git","type":"git"},"_npmVersion":"11.19.0","description":"Run Axle Keys model code locally: build parametric CAD, get the same verdict axlekeys.com gives, export STEP/STL. Offline; warm rebuilds in tens of milliseconds.","directories":{},"maintainers":[{"name":"hkhenning","email":"hkrishenning@gmail.com"}],"_nodeVersion":"24.21.0","dependencies":{"replicad":"1.0.1","replicad-opencascadejs":"1.0.0"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cad-engine_0.2.0_1790618945388_0.9244436014885542"}}},"time":{"created":"2026-09-05T17:07:56.265Z","modified":"2026-09-28T18:09:05.795Z","0.1.0":"2026-09-05T17:07:56.565Z","0.1.1":"2026-09-05T17:16:26.418Z","0.1.2":"2026-09-06T22:30:19.959Z","0.1.3":"2026-09-06T23:57:10.652Z","0.1.4":"2026-09-13T00:38:40.342Z","0.1.5":"2026-09-15T14:27:46.220Z","0.2.0":"2026-09-28T18:09:05.514Z"},"bugs":{"url":"https://github.com/AxleKeys/cad-engine/issues"},"license":"MIT","homepage":"https://axlekeys.com","keywords":["cad","parametric","replicad","opencascade","step","stl","geometry","agent","mcp"],"repository":{"url":"git+https://github.com/AxleKeys/cad-engine.git","type":"git"},"description":"Run Axle Keys model code locally: build parametric CAD, get the same verdict axlekeys.com gives, export STEP/STL. Offline; warm rebuilds in tens of milliseconds.","maintainers":[{"name":"hkhenning","email":"hkrishenning@gmail.com"}],"readme":"# Axle Keys — local CAD engine\n\n![side-table, dado-joint, drawer-box — three of the shipped examples, rendered by axlekeys.com's own renderer](media/hero.png)\n\nBuild parametric CAD models in JavaScript, run them on your own machine, and get the\n**same verdict** [axlekeys.com](https://axlekeys.com) gives — offline, and fast enough to\nsit inside a loop rather than beside one. A simple part rebuilds in about 30ms on a warm\nkernel; a cabinet with dado joints cut into both sides costs about 350ms. The kernel starts\nonce, so `watch`, `repl` and `sweep` pay that startup a single time.\n\nThis is what a verdict looks like. Real output, captured by this repo's release check from\nthe example it ships:\n\n```text\n$ node packages/engine/cli.mjs build examples/side-table.js\n✓ built in 470ms — 5 part(s)\n  overall    450.0 ×   450.0 ×   500.0 mm\n\n   Top                  450.0 ×   450.0 ×    25.0\n   Leg 1                 40.0 ×    40.0 ×   475.0\n   Leg 2                 40.0 ×    40.0 ×   475.0\n   Leg 3                 40.0 ×    40.0 ×   475.0\n   Leg 4                 40.0 ×    40.0 ×   475.0\n\n  Solid volumes measured — 5 exactly filling its blank. A part with no note above fills its rectangular blank EXACTLY, so nothing has been taken out of it and a cut you expected there did not land. A part measuring short is either cut or simply not box-shaped (a cylinder is 78.5% of its box); volume cannot separate those, and measure(part) names the number.\n\n  no problems.\n```\n\nPush a parameter past what the model declares and the same command says what went wrong —\nwith the shared volume **measured**, not inferred from bounding boxes:\n\n```text\n$ node packages/engine/cli.mjs build examples/drawer-box.js --param clearance=-1\n✓ built in 893ms — 6 part(s)\n  overall    400.0 ×   470.0 ×   200.0 mm\n\n   Side L                18.0 ×   350.0 ×   200.0\n   Side R                18.0 ×   350.0 ×   200.0\n   Bottom               364.0 ×   350.0 ×    18.0\n   Top                  364.0 ×   350.0 ×    18.0\n   Back                 364.0 ×    18.0 ×   164.0\n   Drawer               366.0 ×   332.0 ×   165.0 · 19.62% of its blank (16,116,408mm³ short)\n\n  Solid volumes measured — 1 measuring short of its blank, 5 exactly filling its blank. A part with no note above fills its rectangular blank EXACTLY, so nothing has been taken out of it and a cut you expected there did not land. A part measuring short is either cut or simply not box-shaped (a cylinder is 78.5% of its box); volume cannot separate those, and measure(part) names the number.\n\n  4 problem(s):\n   [warning] bad_param_range: Parameter 'clearance' default -1 is outside its own range [0:4].\n   [error] part_interference: Parts 'Side L' and 'Drawer' occupy 34,980mm³ of the SAME solid volume (measured intersection, not a bounding-box guess; boxes penetrate 1.0×212.0×165.0mm). Parts must share faces, never volume — cut the pocket/dado out of one of them, or move the other.\n   [error] part_interference: Parts 'Side R' and 'Drawer' occupy 34,980mm³ of the SAME solid volume (measured intersection, not a bounding-box guess; boxes penetrate 1.0×212.0×165.0mm). Parts must share faces, never volume — cut the pocket/dado out of one of them, or move the other.\n   [error] part_interference: Parts 'Top' and 'Drawer' occupy 8,768mm³ of the SAME solid volume (measured intersection, not a bounding-box guess; boxes penetrate 364.0×212.0×1.0mm). Parts must share faces, never volume — cut the pocket/dado out of one of them, or move the other.\n\n  not passed — 3 error(s). An error fails the model.\n```\n\nThis is the real execution path, not a demo: `packages/engine/runner.mjs` is the file the\nhosted geometry service imports, at the same paths, so a stack trace here matches a stack\ntrace there.\n\n## Use\n\n```bash\naxle build   model.js                 # verdict: dimensions, per-part boxes, problems\naxle build   model.js --json          # the same, machine-readable\naxle explain model.js                 # what --param accepts: each parameter, value, range\naxle sweep   model.js --range w=300:800:50 --require passed --score min:volume   # search the space\naxle verify  \"models/**/*.js\"         # build them all; exit with the worst verdict (CI's gate)\naxle watch   model.js                 # rebuild on every save, ~55ms\naxle repl                             # paths on stdin, one JSON verdict per line\naxle export  model.js --step --stl    # real CAD files\naxle build   model.js --param topSize=700\naxle check-dimensions model.js --width 450 --height 500   # overall size against numbers you typed (mm)\naxle measure model.js --part-a \"Top\" --part-b \"Leg 1\"     # gap / flush / overlap, measured shared volume\n```\n\n⏱ **Use `watch` or `repl` for real work.** A one-shot `axle build` spends ~900ms starting\nNode and the OpenCascade kernel to do ~100ms of building. The persistent modes pay that once\nand then rebuild in ~55ms — measured at **19.8× faster** than the one-shot on the same machine.\n\n`explain` is the door into that: it prints every parameter with its declared range without\nbuilding, so an agent can sweep a model it has never opened. `explain --json` returns the\nsame `params` array `build --json` carries.\n\n## Search a space\n\nStop drawing one design and ask the whole range. `axle sweep` builds every variant of a\nparameter space **on one warm kernel** — the first build pays for OpenCascade, the rest cost\n~55ms each — and ranks them by a **measured** field of the verdict, never an opinion:\n`volume` (mm³), `overall.x|y|z`, `parts`, `problems`, `buildMs`, or a swept parameter's\nown name. `--require passed` drops the variants whose verdict failed — an `error` — *before*\nranking; `--where overall.x<=800` constrains on the same terms, and `--where problems==0` drops\nevery variant with any finding. `problems` counts EVERY finding the geometry checks reported on\nthat variant — errors, warnings, info and `unverified` ones — and on a big model an unverified\nfinding can depend on how busy the machine was, so read what a `problems==0` sweep excluded\nbefore you trust it. The model's static lint is reported **once** in the sweep's header, never\nper variant, so it cannot move a ranking or a filter. Real output, captured by this repo's\nrelease check — the widest `slatPitch` the checks have nothing to say about, and the first\none they flag:\n\n```text\n$ node packages/engine/cli.mjs sweep examples/slat-bench.js --range slatPitch=70:170:10 --where problems==0 --score max:slatPitch\nslat-bench.js — 11 variant(s) swept in 1183ms · 11 built · 11 passed · 2 excluded by --where problems==0 · 9 ranked by max:slatPitch\n\n  static lint: none (the model as handed in, checked once for the sweep).\n\n   #  slatPitch  passed  problems  overall (mm)            volume (mm³)\n   1        150  ✓              0  1410.0 × 400.0 × 450.0      16420000\n   2        140  ✓              0  1320.0 × 400.0 × 450.0      16420000\n   3        130  ✓              0  1320.0 × 400.0 × 450.0      16420000\n   4        120  ✓              0  1320.0 × 400.0 × 450.0      16420000\n   5        110  ✓              0  1320.0 × 400.0 × 450.0      16420000\n   6        100  ✓              0  1320.0 × 400.0 × 450.0      16420000\n   7         90  ✓              0  1320.0 × 400.0 × 450.0      16420000\n   8         80  ✓              0  1320.0 × 400.0 × 450.0      16420000\n   9         70  ✓              0  1320.0 × 400.0 × 450.0      16420000\n\n  winner   --param slatPitch=150\n\n  excluded (2):\n   slatPitch=160  missed problems==0 — unsupported_part ×2\n   slatPitch=170  missed problems==0 — unsupported_part ×2\n```\n\nThe winner line is ready to paste into `axle build`. `--json` prints one record per variant\nin ranked order and a final `{ summary }`; two runs of the same sweep are byte-identical apart\nfrom the timing fields, so a sweep is a fixture as much as a search. With no `--range`, every\nparameter `explain` lists is swept over its declared range; a sweep bigger than\n`--max-variants` (500) is refused before anything builds.\n\n## Verify a whole repository — and let CI hold the line\n\n`axle verify` builds every model a pattern names, on one warm kernel, and exits with the\n**worst** verdict it found: `0` passed · `1` built with errors · `2` did not build · `3`\ncould not run. A warning is reported and annotated, and does not fail the run. That is a CI\ngate, so there is a GitHub Action for it — a thin wrapper around\nthat one command, and nothing else:\n\n```yaml\n      - uses: actions/checkout@v4\n      - uses: AxleKeys/cad-engine@main\n        with:\n          models: \"models/**/*.js\"\n```\n\nEach problem becomes an annotation on the line of the diff that caused it. A pattern that\nmatches **no files** fails the run rather than passing quietly — a green job that verified\nzero models reads exactly like \"every model is clean\", and that is the one thing CI must\nnever say. Full guide, including a `--param` matrix: [`docs/ci.md`](docs/ci.md).\n\nThis repository runs that Action on the examples below, through the published package, on\nevery push — including one job whose fixture is **expected to fail** and which then asserts\nthat it did. [See the runs.](https://github.com/AxleKeys/cad-engine/actions)\n\n## Examples\n\nEach one shows a single check being honest. Every row is a fixture `npm test` runs, with the\nverdict it claims; every **Try** is a command whose result this repo's release check asserts.\n\n| Example | What it shows | Try |\n|---|---|---|\n| [`examples/side-table.js`](examples/side-table.js) | The on-ramp. Four legs that stop exactly at the top's underside — a clean verdict with every part's box. | `axle build examples/side-table.js` → clean |\n| [`examples/bookshelf-3-shelf.js`](examples/bookshelf-3-shelf.js) | Shelves that butt against the sides. Shared faces are contact, not collision, so the verdict stays clean. | `axle build examples/bookshelf-3-shelf.js` → clean |\n| [`examples/dado-joint.js`](examples/dado-joint.js) | A real dado joint PASSES `part_interference`: each shelf's tongue sits inside a groove, sharing faces on five sides and no volume. The old bounding-box rule called this a collision. | `axle build examples/dado-joint.js` → clean |\n| [`examples/dado-interference.js`](examples/dado-interference.js) | The same cabinet with the grooves never cut, so every shelf end is buried 6mm inside a side. The verdict prints the shared 32,400mm³ per joint — measured, not guessed. `npm test` keeps this one loud. | `axle build examples/dado-interference.js` → `part_interference` |\n| [`examples/drawer-box.js`](examples/drawer-box.js) | What a `--param` is for. Clean at every `clearance` in its declared range, where the drawer touches or clears its opening; push it below the range and the verdict measures the collision. | `axle build examples/drawer-box.js --param clearance=-1` → `part_interference` |\n| [`examples/slat-bench.js`](examples/slat-bench.js) | `unsupported_part` doing its job: widen `slatPitch` past its range and the end slats sit beyond the rails. It is a warning — the verdict flags the model and still passes it. `railLength` is a plain const, which `axle explain` lists as not adjustable. | `axle build examples/slat-bench.js --param slatPitch=160` → `unsupported_part` |\n\n## A model\n\nA model is a function that returns named parts. Parameters are annotated consts, which is what\nmakes `--param` work — a dimension **is** a parameter, not a coordinate to edit. Primitives are\ncentred in X and Y and sit on z = 0; **X is width, Y is depth, Z is height.**\n\n```js\nconst topSize = 450;      // [300:800]\nconst height  = 500;      // [350:700]\n\nconst main = ({ makeBaseBox }) => {\n  const top = makeBaseBox(topSize, topSize, 25).translateZ(height - 25);\n  return [{ name: \"Top\", shape: top }];\n};\n```\n\nA numeric const with no `// [min:max]` is not a parameter: `--param` refuses it and `explain`\nlists it as not adjustable. The range is what the model declares, not a limit the CLI\nenforces — which is exactly what makes the drawer example above work.\n\n## What the verdict checks\n\nDeterministic rules, the same implementations the hosted platform runs. Every finding carries\na severity, and the severity decides: an **error** fails the model (`axle build` and\n`axle verify` exit 1), a **warning** flags it and it still passes.\n\n- **`part_interference`** (error) — a *measured* solid intersection between two parts, not a\n  bounding-box guess. A dado joint shares faces and stays silent; a collision is reported with\n  the shared volume in mm³.\n- **`unsupported_part`** (warning) — a part with nothing beneath it. Conservative by design,\n  and it only understands bearing from below, so it can flag legitimate end-supported joinery;\n  that is why it flags rather than fails.\n- **`unclosed_volume`**, **`non_positive_volume`** (errors) — read from the B-rep, not the\n  mesh: a fillet that ate its stock, a shell that turned a solid inside out. Both build, both\n  mesh, both pass a dimension check, and neither is a solid.\n- **`empty_mesh`**, **`degenerate_dimension`**, **`empty_parts_array`** (errors).\n\nA finding marked `unverified` means the build could not measure that pair in the time it\nallowed itself. It is reported, and it does **not** fail the model, whatever its severity — \"I\ncould not tell\" is not \"there is a defect\".\n\nBeside the geometry, the verdict carries the **static lint** the platform's push verdict\ncarries: rules about the code's shape, read without running it, through the same function\nthat composes them there. Five are errors — `main_missing`, `global_replicad`,\n`code_truncated`, `rotate_no_args`, `extrude_no_args` — and six are warnings —\n`params_argument`, `no_parameters`, `too_many_parameters`, `bad_param_range`,\n`generic_body_name`, `magic_numbers`. On a model that built, `main_missing` is dropped and\n`code_truncated` becomes a warning, because the build itself contradicts them; on one that did\nnot build they stand at full strength, because they often name why. `sweep` reports the lint\n**once** for the whole sweep (its header, and `summary.lint` in `--json`), over the model as you\nhanded it in: it is in no variant's `problems` or `passed`, so it cannot rank, filter or\nexclude a variant.\n\n`axle build` also prints each part's **solid volume against the rectangular blank it would be\ncut from** — the sentence the platform prints under the same part. Exactly 100% proves nothing\nwas taken out of the part; less than 100% is a cut *or* a part that was never box-shaped (a\ncylinder fills 78.5% of its box), and volume cannot tell those apart. It is a measured fact and\nnever a gate.\n\n## Check a size, measure two parts\n\nTwo of the platform's probes answer here too, computed by the same functions its MCP tools call:\n\n- **`axle check-dimensions model.js --width W [--depth D] [--height H] [--tolerance-mm T]`** —\n  the overall box against numbers you type (mm; width = X, depth = Y, height = Z). The default\n  band is ±5% or 3mm per axis, whichever is larger, and it is deliberately loose;\n  `--tolerance-mm` replaces it outright on every axis. A negative or non-numeric tolerance is\n  refused, never swapped for the default, and every answer names the band it applied. Exit `0`\n  within the band, `1` off, `2` did not build, `3` could not run. `--json` prints the\n  `check_dimensions` tool's answer.\n- **`axle measure model.js --part-a A [--part-b B]`** — one part's box and its solid volume\n  against its blank; or, for a pair, the per-axis gap / flush / overlap, the nearest distance,\n  and `shared_volume`: the real intersection this build measured, which tells a dado (shared\n  faces, no volume) from a collision (volume). Boxes are world-axis and conservative for\n  rotated or notched parts; `shared_volume` is the number to trust. `--json` prints the\n  `measure` tool's answer.\n\nThe targets are yours: nothing here reads a brief stored on the platform.\n\n## Install\n\nRequires **Node 23 or newer**. The engine ships as plain JavaScript: the repository's\nTypeScript with its type annotations erased at export by Node's own stripper, positions kept,\nso a line number here is the line number there. No bundler, no build step. The CLI checks the\nNode version first and tells you so rather than failing inside a module loader.\n\n```bash\nnpm install -g @axle-keys/cad-engine      # or: npx -y @axle-keys/cad-engine build side-table.js\ncurl -O https://raw.githubusercontent.com/AxleKeys/cad-engine/main/examples/side-table.js\naxle build side-table.js\n```\n\nThat last line is run by this repo's release check before every publish — the packed tarball\nis installed into a fresh project and the command is executed through the installed bin — and\n`npx -y @axle-keys/cad-engine@<version> build side-table.js` is run against the registry itself after each one.\n\nOr clone it:\n\n```bash\ngit clone https://github.com/AxleKeys/cad-engine.git\ncd cad-engine\nnpm install\nnode packages/engine/cli.mjs build examples/side-table.js\n```\n\nThat last line is the command this repo's own release check runs before publishing, so it is\nknown to work on the files you just cloned — and it is the first capture at the top of this\npage.\n\nTo get `axle` on your PATH instead of typing the full path:\n\n```bash\nnpm link          # from the repo root\naxle build examples/side-table.js\n```\n\n## Check what you installed\n\n```bash\nnpm test\n```\n\nTwo legs. The first builds **every example above** and asserts each gives the verdict its row\nclaims — the clean ones clean, the loud ones loud, and each *Try* command producing the rule it\nnames. The second is a **positive control**: it builds a model whose two parts deliberately\nshare 62,500mm³ of solid volume and asserts the interference check *reports* it. That one\nmatters more than it looks. A verifier that only ever sees a clean model prints the same\nsuccess whether the checks are running or silently dead, so the failing case is tested too.\n\nIt tells you your copy runs and that its checks are alive. It does **not** call\n[axlekeys.com](https://axlekeys.com), so it does not measure agreement with the hosted verdict —\nthat rests on this package being generated from the same source the platform runs.\n\n## Docs — the modelling rules, as files\n\n[`docs/`](docs/) holds the 18 sections of the skill pack the hosted platform teaches\nits agents — the API it writes against, the axis convention, the joinery and domain rules,\nthe worked examples — rendered from the same list the pack is deployed from, so what you grep\nhere is what an agent connected over MCP is told. Start at\n[`docs/MasterSkill.md`](docs/MasterSkill.md). Anything a section says to check, `axle build`\nchecks.\n\n## Skills — teach your agent the whole workflow\n\nThe engine above runs on **your** machine. These skills are the other half: choreography for\nan agent driving the hosted platform at [axlekeys.com](https://axlekeys.com), over MCP.\n\n| Skill | Tier | What it does |\n|---|---|---|\n| [`brief-to-model`](skills/brief-to-model/) | Free | Turn a plain-English description into a real parametric 3D CAD model — built, dimension-verified, and shown back as a picture you can spin. |\n| [`design-loop`](skills/design-loop/) | Free | Verify an agent-built CAD model with independent critics instead of self-review — clean-context reviewers that fetch the reference and probe the geometry themselves, then a punch list you gate on. |\n| [`local-loop`](skills/local-loop/) | Free | Iterate on an Axle Keys model on your own machine at tens of milliseconds a build — the same engine and the same verdict axlekeys.com runs — then push the finished model once instead of a round trip per attempt. |\n| [`parametric-fit`](skills/parametric-fit/) | Free | Make an existing parametric CAD model fit a real space or a hard number — an alcove, the gap under a window, a maximum depth — by moving the parameters it already declares and PROVING the result with a dimension check, never by editing geometry. |\n\nEvery skill here is completable on the **free tier** — that is enforced at export time, not\npromised, so nothing in this repo walks you into a paywall.\n\nInstall as a plugin, which brings the skills with it:\n\n```bash\nclaude plugin marketplace add AxleKeys/cad-engine\nclaude plugin install axle-cad-engine@axle-keys\n```\n\nOr take a single skill the way your agent takes skills — for Claude Code, drop the folder in\n`.claude/skills/`. Either way, connect Axle:\n\n```\nhttps://api.axlekeys.com/mcp\n```\n\n## What this is not\n\nThis runs models. It does not store them.\n\nPersistence, versions, the 3D viewport, assemblies, motion, configurators, shop drawings, cut\nlists and nesting, materials and textures, the knowledge layer, and the MCP interface your\nagent drives all live on [axlekeys.com](https://axlekeys.com).\n\n> **This package is a calculator; the platform is the notebook.** It does sums. It doesn't\n> remember anything, show you anything, or let you sell anything.\n\nIt is also **read and run, never write**. Local edits change your copy, not Axle — if the two\never disagree about the same model, the server is the one that counts.\n\n## Contents\n\nGenerated from the Axle Keys repository — 27 source files, derived by walking the\nengine's own imports; 6 examples; 18 docs sections; 4 skills. The picture at the\ntop was rendered by a committed script through the platform's own headless renderer, and the\nexport refuses a picture whose recorded source no longer matches the examples beside it. Do\nnot hand-edit; changes are made upstream and re-exported.\n\nPinned: `replicad@1.0.1` · `replicad-opencascadejs@1.0.0`\n\n## License\n\nMIT.\n","readmeFilename":"README.md"}