NativeCAD MCP server
Release notes
What changed in every nativecad-mcp release, straight from the project's own CHANGELOG — newest first, each version with its date, its downloads and their checksums. Link to a version with its anchor (#v0-27-0).
Current release
v0.27.0 “Hannigan” · October 28, 2026
Install from the command line: npx -y https://featureboard.ai/nativecad/releases/nativecad-mcp-0.27.0.tgz — drop the .mcpb into Claude Desktop or Cowork, or open the .plugin in a Claude chat. Every way in is on the For AI page; latest.json carries the same files with bytes and sha256 for a script to verify.
- 0.27.0 “Hannigan”
- 0.26.0 “Graf”
- 0.25.0 “Glover”
- 0.24.0 “Gelmetti”
- 0.23.3 “Gatti”
- 0.23.2 “Frühbeck de Burgos”
- 0.23.1 “Fedoseyev”
- 0.23.0 “Eschenbach”
- 0.22.1 “Dutoit”
- 0.22.0 “de Waart”
- 0.21.0 “DePreist”
- 0.20.0 “de la Parra”
- 0.19.0 “Christie”
- 0.18.3 “Chan”
- 0.18.2 “Canellakis”
- 0.18.1 “Bülow”
- 0.18.0 “Brico”
- 0.17.2 “Berglund”
- 0.17.1 “Bátiz”
- 0.17.0 “Asahina”
- 0.16.0 “Talich”
- 0.15.0 “Svetlanov”
- 0.14.1 “Sawallisch”
- 0.14.0 “Rozhdestvensky”
- 0.13.0 “Rostropovich”
- 0.12.4 “Masur”
- 0.12.3 “Marriner”
- 0.12.2 “Mäkelä”
- 0.12.1 “Kertész”
- 0.12.0 “Jurowski”
- 0.11.2 “Jochum”
- 0.11.1 “Järvi”
- 0.11.0 “Hrůša”
- 0.10.0 “Honeck”
- 0.9.0 “Hogwood”
- 0.8.0 “Fricsay”
- 0.7.0 “Fischer”
- 0.6.0 “Elder”
- 0.5.0 “Doráti”
- 0.4.0 “Dohnányi”
- 0.3.9 “Davis”
- 0.3.8 “Chung”
- 0.3.8 “Chung”
- 0.3.7 “Bychkov”
- 0.3.5 “Blomstedt”
- 0.3.4 “Ashkenazy”
NativeCAD 0.27.x
v0.27.0 “Hannigan”
Scan to simulation: a room scan (.ply) in, an articulated model a robot can use out (cad_scan_to_sim). cad_import reads PLY meshes. cad_crease_repair mends B-spline faces that Parasolid-based CAD would split, and STEP export warns about them. cad_export_body writes QIF 3.0 inspection plans. Rotating zones and transient runs in cad_wind_tunnel. 187 tools (185 in 0.26.0): cad_scan_to_sim and cad_crease_repair are new. The command schema moves 1.68.0 → 1.71.0 (wind_tunnel rotatingZones and mode:'transient'; fit continuity and minSpan; the crease_repair op). Every 1.68.0 document validates unchanged.
New: cad_scan_to_sim
- A LiDAR or photogrammetry scan of a room in, one assembly out. Input is a
.plypoint cloud or mesh, ascii or binary, in metres. A cloud over 100 units across is read as millimetres, and the result says so. The scan is levelled (gravity +Z) and squared to its walls. Floor, walls and ceiling are fitted, and thin panels are split into partitions, floor-anchored pilasters and hung doors, with the hinge side read from the pull. The remaining objects are clustered, recognised and fitted to parametric families. The first family set is a commercial restroom with toilet stalls: toilets, grab bars, tissue rolls, vanity and basins, soap and towel dispensers, waste bins and ceiling lights. An object no rule recognises stays in the model as a static box, never dropped. - Levels of detail L0–L4, each written as MJCF (MuJoCo), URDF and
assembly.json: - L0: the room shell.
- L1: the layout.
- L2: articulated fixtures. Door hinges have a 90° stop, damping and friction. Seat hinges, spring-return flush levers and spinning rolls work, the paper tears at its perforations, and the waste bin is a free body.
- L3: hardware, including a latch bolt that holds its door against the keeper.
- L4: fasteners, which are visual-only in the simulator but count toward mass.
- STEP: the highest level asked is also written as STEP, built by this kernel. Fixtures are exact B-reps: a lofted elongated bowl, a flushometer, undermount basins, a gooseneck faucet, swept grab bars, holders, a stainless bin shell. MJCF and URDF draw them as meshes. The toilet bowl collides as 100 convex pieces, so its cavity is open. Every part carries its material, mass, whether it collides, and its provenance:
observed(fitted to points) orinferred(placed by an installation rule). - The room is closed with stated assumptions (
complete:falseskips this). An entrance door is placed and sized by the 2010 ADA Standards: a 36 in leaf on the front wall, clear of every fixture and of the 404.2.4 maneuvering space, with a lever at 38 in and a closer at ≤ 5 lbf that takes ≥ 5 s from 90° to 12°. Ceiling lights are assumed if none were seen. Each assumption comes back as a sentence inassumptions. adareports the room's first-look ADA dimensions, pass or miss with the measured value: door width, hardware and force, turning circle, compartments, grab bars, seat, paper dispenser, lavatory rim and knee clearance, mirror, reach ranges.sim:trueexercises every model in MuJoCo and PyBullet. It checks rest, door swing to its stop, the latch holding, the seat, the flush lever, the roll, a paper tear, a reach into the bowl, a bin push, and the entrance closer measured against the ADA limits. It needs a python withmujocoandpybullet(SCAN2SIM_PYTHON; on Windows, WSLUbuntu-24.04:/root/simenv). A missing simulator is reported, not failed. The checker ships inside the bundle and is written to a temp file to run.importLevelloads that level's STEP into the session.- Fixture shapes are cached in
$SCAN2SIM_SHAPE_CACHE(default<tmp>/nativecad-scan2sim-shapes); the first build of a fixture takes up to ~30 s. - New refusal codes:
E_INVALID_ARGSfor a missing or non-.plyfile, with the PLY reader'sreason(PLY_NOT_PLY,PLY_NO_VERTICES,PLY_TRUNCATED,PLY_UNSUPPORTED).E_GEOMETRYwithdetails.reasonSCAN_ROOM_INCOMPLETE: the scan gives no usable room. It must show the floor, the ceiling and at least two walls, at about 1 point per cm² or more. A scan thinned to one point in six has no ceiling plane.SCAN_RECONSTRUCT_FAILEDfor any other reconstruction failure.- The sample scan on the showcase page (
scan_tripod_sample.ply) is one point in two, 343,524 points, and reconstructs completely. The chest-mounted sample is thinned for viewing only. - Sparse scans and stall doors. On a sparse scan the door edge fit can latch onto a gap in the panel. A door whose edges cover less than half its opening now fills the opening between its pilasters, and a flag says so. Before this, the fit could produce a 12 mm door with no height, and its zero-height box made the executor refuse the whole STEP. A refused STEP document now reports the executor's reason in
models.L<k>.step.firstError.
Importers
cad_importreads.plymeshes like STL and OBJ;toBrepworks. A PLY with no faces (a point cloud) is refusedPLY_POINT_CLOUD, and the message points atcad_scan_to_sim.- Faster Parasolid X_T tessellation. Long thin helical thread flanks triangulate along their long axis (
CADSOLVER_NO_CDT_LONG_AXIS=1turns it off). A ruled direction no longer gets a sag row at every trim vertex (CADSOLVER_NO_101050_RULED_SIDE_SKIP=1). Cylinder and cone covers pace their rows by the circle, not by a dense rim (CADSOLVER_NO_101051_COVER_ROWS=1). The McMaster thumb screw takes 8 s, down from 72 s. The 151-file X_T corpus takes 129 s, down from 250 s, with 4.39M triangles instead of 5.22M and no open-edge changes.
Freeform surfaces: G1 creases
- New tool
cad_crease_repair(opcrease_repair). Each interior knot line of a B-spline face that breaks G1 is either smoothed or split into a real edge: - Smoothed: one control row moves to the C1 position, then one knot copy is removed exactly, when the move is within
smoothTol. - Split: the face is cut into G1 sub-faces on the same surface, which loses nothing.
modeisauto,smoothorsplit. Afacesfilter is optional. The shell is audited before publish, and the whole repair rolls back on any failure.- Why it matters: a Parasolid-based reader (Onshape, SolidWorks, NX, Solid Edge) splits such a face, and a creased pillar became ~26,000 faces.
cad_export_bodyformat:"step"checks interior G1 on the exported bodies. By default it warns, namingcad_crease_repair. Understrict:trueor a handoff mark it refusesE_GEOMETRYexport_interior_g1and writes nothing.CADSOLVER_NO_KNOT_G1_CHECK=1turns the check off.checkFullgains two warnings:interior_g1_discontinuity, once per creased face.edge_off_face, for an edge that leaves a face it bounds by more than that face's own tolerance (CADSOLVER_NO_100990_EDGE_ON_FACE=1).cad_helphas acontinuitytopic. The freeform verbs report the continuity they built, and fits take a continuity target andminSpan(schema 1.70.0).
Export
cad_export_bodyformat:"qif"writes a QIF 3.0 inspection plan from the document's authored PMI. The reply carries counts,byMethod,skipped,unmappedFaces, the unverified elements and the validation verdict. A document with no PMI is refusedE_PRECONDITIONand nothing is written.- For the shipped bundle, the QIF writer and the inspection plan load their modules by static path, print their CLI output only when run as a script, and read the measurement-method table as a staged asset. As first landed, they kept the server from being bundled at all.
CFD (cad_wind_tunnel)
- Rotating zones and transient runs.
rotatingZones: [{ bodies, axis, centre, omega | rpm, radius?, halfHeight? }]takes up to 16 zones, andmode:'transient'takes exactly one ofdurationorrevolutions, with optionalsampleEveryanddryRun. - Solver: zones are solved by overset grids on the GPU, with the CPU reference behind the LBM budget.
- Output: each rotor gets cycle-averaged thrust, torque, power, CT/CQ/CP and FM (hover), plus phase-resolved series and a per-revolution convergence record.
- Refusals: each is named:
rotating-axis-degenerate,rotating-zone-overlap,rotating-zone-clips-static-body,rotating-transient-needs-gpu,rotating-zone-too-thin-for-overset,rotating-zone-near-boundary, and the rest of the set. - The LBM core runs past 8 GB. One population set is streamed in place, and 16-bit population storage halves the bytes per node (
CADSOLVER_LBM_STORAGE=fp32default,fp16,fp16c).
Booleans and meshing
- A spline face's geometry no longer depends on cavities the cut never touched.
- Plane-on-cone sections re-curve as exact ellipses and circles.
- Chord-fin piece guards refuse only drops that cut a piece off.
- Cut-body gates read each mesh's own chord deficit.
- A half-kept face is never reported untouched.
- Paired raw subtracts keep their cut walls exact.
- Loft step handling follows the trend on swept lofts.
- The SSI separation certificate tries the plane normal, so a grazing B-spline wall is proved empty in ~0.1 s.
Tool listing
- The
tools/listline moves from 396 KiB to 401 KiB (ceiling 403 → 408 KiB) for the two new tools. The listing is 408,955 B across 187 tools.
NativeCAD 0.26.x
v0.26.0 “Graf”
Fixes an extra body in exports after a curved boolean, present in 0.24.0 and 0.25.0. CATIA V5 parts now open as exact solids. Creo assemblies, binary ACIS .sab and Rhino 2 files open. STEP export writes pcurves on B-spline faces, and metre-unit files tessellate like their millimetre twins. 185 tools, the same as 0.25.0. The command schema stays at 1.68.0; two optional fields were added without a version move (import exactBrep, dfm_check machiningStockMm), so every 1.68.0 document validates unchanged. The import census now covers 23,347 user-facing files; 92 % of them opened at the census, before the later CATIA, ACIS and Rhino waves.
Fixed: an extra body in exports after a curved boolean (0.24.0, 0.25.0)
- A general-engine union, subtract or intersect could leave a second live solid in the model (
ops/genBool.js). The identity check builds the complement of the answer (A ∩ B for a subtract) to check the volumes. When that complement came from the answer's own classify and its trimmed faces were carried, the faceted complement body stayed live. Every exporter that writes all live solids then wrote it too: STEP, 3MF, GLB and mesh payloads. A sphere r16 minus a through-cylinder r5 wrote twoMANIFOLD_SOLID_BREP: the exact ring (14,706.407 mm³) and a 2,447.09 mm³ faceted A ∩ B. The leak came in with 2026-10-20, so it is in 0.24.0 and 0.25.0. - The builder now retires every solid it made and does not return, including when it returns nothing or throws. The published solid and the identity record (
complementSource, volumes,deltaPct) are byte-identical to 0.25.0. Only the stray body is gone. - A session saved by 0.24.0 or 0.25.0 can still hold the stray body. Rebuild the model, or find the extra body with
cad_list_model_stateand remove it withcad_delete_body, before exporting again. CADSOLVER_KEEP_BOOL_INTERMEDIATES=1(a debugging switch) still keeps the intermediates.- A boolean's retired intermediates are purged when the op ends (
ops/retiredPurge.js). A rim union's document went from 134,532 entities to 2,376. Fillet-registry faces are kept, so a later re-fillet still finds them.CADSOLVER_NO_OP_PURGE=1turns it off.
Importers (cad_import)
- CATIA V5
.CATPartopens as an exact solid (importers/CatiaCgm.js,CatiaBrep.js). The B-rep is decoded from the part's own CGM stream (clean-room, from file bytes only). A part opens assource:'catia-cgm',brep:trueonly when its solid closes and agrees with its own display mesh. Otherwise it opens as the display mesh, as in 0.25.0, with the reason onbrepSubCode. - Results: 251 of the 306 CATParts in the census ledger are exact (0 in 0.25.0), and 19 of 20 NIST CATParts. Across all 2,659 CATIA rows in the census, 1,571 are exact.
- New
importfieldexactBrep(default true).exactBrep:falsegives the display mesh as before. - New
brepSubCodevalues:CATPART_BREP_DISPLAY_MISMATCH,CATPART_BREP_DISPLAY_UNAVAILABLE,CATPART_BREP_EDGE_GAP,CATPART_BREP_NO_SOLID,CATPART_BREP_PCURVE_UNSUPPORTED,CATPART_BREP_SHELL_INCONSISTENT,CATPART_BREP_SHELL_OPEN,CATPART_BREP_STREAM_UNDECODED,CATPART_BREP_SURFACE_UNSUPPORTED,CATPART_BREP_TOPOLOGY_UNDECODEDandCATPART_BREP_VERTEX_UNDECODED.CATPART_BREP_CGMis unchanged. - The decoder's alternative readings are budgeted by counted work, not wall-clock time, so a part opens the same way on a loaded machine. V5R14 to V5R18 display meshes whose strip lengths are in the index width now decode (90 of 107 that refused
CATPART_DISPLAY_UNDECODED)..CATProductcomponent placement is unchanged. - Creo assemblies (
.asm) open with their components placed (importers/CreoAssembly.js). Components resolve by name and highest version from the assembly's folder, and sub-assemblies compose.searchPathsis now accepted for.asm. 216 of 369 corpus assemblies open (none in 0.25.0). - A refusal keeps
CREO_ASSEMBLY_NO_GEOMETRY, with the new subCodesCREO_ASSEMBLY_COMPONENTS_MISSING,CREO_ASSEMBLY_COMPONENTS_UNBUILT,CREO_ASSEMBLY_EMPTYandCREO_SECTION_SCRAMBLED. A placed component that does not close is left out and named. - Creo parts:
cyl_splsrf(threads and springs),b_splsrf, and splsrf withtan_cond [3,3]are read. Faces stored underActDatumsare read, which accounts for most ofCREO_SHELL_OPEN. Construction faces (cosmetic threads, sketch regions) are left out of the solid instead of refusingCREO_TOPOLOGY_BROKEN. Stored mass properties older than the geometry are no longer used. New refusals:CREO_SECTION_SCRAMBLEDandCREO_FACETED_ONLY; an emptyVisibGeomnow refusesCREO_NO_SOLID_GEOMETRY. 1,400 of 1,873 corpus.prtopen (1,213 at 0.25.0). - NX
.prtfrom before NX 11: OLE2 parts that keep an uncompressed Parasolid transmit in/UG_PART/UG_PARTopen. An OLE2 part whose/part/arrangementsnames an assembly says so.CADSOLVER_NO_NX_OLE2_RAW=1turns it off. - ACIS (
importers/SatProcedural.js): - Binary
.sabis now an import extension (read through SatReader). It was refused as foreign in 0.25.0. - Procedural
sumsurfaces are rebuilt as exact NURBS, and parametricsummarycurves are refitted. ACIS 27 records are read. Wire-only files open as wire bodies. Loopless sphere faces build as two hemispheres. - New refusals:
SAT_EMPTY_MODEL(a header-only healer output), andSAT_UNSUPPORTED_SURFACEfor a lemon torus (negative major radius). - Results:
.satopens 955 of 981 (825 before), and.sab2 of 2. Kill switches:CADSOLVER_NO_SAT_PROCEDURAL,CADSOLVER_NO_SAT_WIRES. - Rhino
.3dm: Rhino 2 B-rep, curve and point records, the 1.2 extrusion layout, and older edge, plane and mesh layouts are read. A mesh-only file opens as a display mesh, and a curve-only file opens as wire bodies instead of throwing. New refusals:RHINO_EMPTY_MODEL,RHINO_POINTS_ONLY,RHINO_ANNOTATION_ONLYandRHINO_INSTANCE_REFS_ONLY. 900 of 937 corpus files open (730 before), 501 of them exact (483 before). - The import cache keys on the environment. Every
CADSOLVER_*variable is part of the key, so a flipped switch no longer replays an earlier result.
Booleans
- Butted coaxial pipe runs union exactly (
ops/capButtUnion.js). Two solids that meet on one opposed, coincident cap pair are glued on that cap. Before, the union re-tiled the tangent walls as 612 facets: 890 faces,geometryDegraded. Now it gives 16 exact faces with volume = vol(A) + vol(B). New refusals:capbutt_extra_contactandcapbutt_not_separated.CADSOLVER_NO_100893_CAP_BUTT_GLUE=1turns it off. - A subtract carries the cut cylinder or cone exact beside an untouched cylinder. A blind r9 bore in an R323 wall removed 1,240.379 mm³ against a closed form of 1,009.898. It now removes 1,018.215 at the default mesh and 1,008.471 at chord 0.002.
- Near-tangent results: the near-tangent sphere intersect is proven EMPTY again, and the matching subtract refuses
no_material_removedinstead of publishing a 0.0017 mm³ cut. - Pierce:
- ELLIPSE edges are sampled on the curve, so a cap that crosses its face imprints instead of leaving two coincident faces.
- Pierce no longer publishes a multi-lump union as one solid.
- The 32-plane pierce bound now refuses
pierce_plane_count. Before, it refusednon_parallel_pierce, which still names a real chain failure. - Curved trims are re-fitted in more cases:
- PLANE | B-spline chord chains. Louvred spline endplates now have an exact volume in 45 of 45 cases, 18 before. That volume agrees with OCCT to 3.7e-5.
- Shallow cone/cylinder crossings.
- Sparse and shallow chains are densified, and fitted-edge mesh samples are snapped onto both carriers.
- A crease guard keeps tangent loft seams on their chords. A wing-panel bore that 0.25.0 refused
boolean_under_removalnow builds. - Also fixed:
- A second lug on a revolved shell's seam plane unions exact and watertight.
- A plane cutting a cylinder mid-height is no longer taken for a cap rail.
- The profile hole/outer touch tolerance in
extrudeis applied. - Fillets:
- The mixed tangent-chain rung is kept only when it rounds at least as many edges as the ordinary path. nist_ctc_01's all-edges r0.5 fillet rounds 88 edges again, after 69 in 0.25.0.
- Fillet and blend results with B-spline faces report the quadrature volume, like their operands.
Tessellation
- Metre and other declared-unit files tessellate like their millimetre twins. Five millimetre constants were applied in model units, so on a metre file they were 1000 times too large or too small:
- the sliver-edge rule. The P424 pillar's outer skin is back on OCCT: 2,862,476 against 2,862,587 mm³.
- the CDT input snap. P424 wing tips mesh watertight without repair fill.
- the 0.005 mm spline sag floor.
- the weld radius.
- the arc closure and coincidence constants.
Millimetre and unit-less files are unchanged.
- C0 knots are grid lines and edge samples. A polyline-ruled B-spline face no longer chords across its creases: 0.776 mm off the surface before, 5.7e-13 mm now. Meshes of such faces are larger.
- Measurements get their own knot-grid budget. Shell gaps and checkFull no longer depend on how often the document was tessellated before.
- Other fixes:
- nist_ctc_04 winding conflicts 43 -> 0.
- Spline grids no longer mint sites the CDT drops, and hole walls no longer mint sites the periodic cover drops.
- A cone's fan apex no longer drives 485 false refinement points; the wall had read 233.05 mm³ against an exact 261.80.
- Sphere bands between two latitude rings take the 0.01 mm sagitta rule. A napkin ring reads -0.75 % instead of -1.43 %.
- Floored cone walls refine the same on both mirror-image halves.
- Sliver repair in the solid FEA mesher matches the whole-mesh result in about 60 % of the time.
- checkFull and the STEP void split report
shell_gap_below_tolerancewhen two shells of one solid are closer than the tolerance, measured on the B-rep. A closed spline-loft cavity now splits intoBREP_WITH_VOIDS.
STEP export
- PCURVEs on B-spline faces (
packages/interop/src/exporters/stepPcurves.js). OCCT-based readers (gmsh, FreeCAD, build123d) no longer have to project every edge onto its NURBS face; on one flap that projection missed by up to 4.4 mm. - Boundary iso-line edges and interior trims each get a (u,v) pcurve, written only when it maps onto the 3-D edge within 1e-7 mm. On a chord-corner flap, OCCT's InvalidCurveOnSurface count went from 136 to 18.
- Edges spliced verbatim from an imported file are left alone, so an unchanged round trip is byte-identical.
CADSOLVER_NO_STEP_PCURVE=1writes the old bytes.CADSOLVER_NO_100910_INTERIOR_PCURVES=1turns off only the interior trims.- Metre exports declare the tolerance in metres:
UNCERTAINTY_MEASURE, the.tol.jsonsidecar and the in-file tolerance block. A shelled B-spline tube no longer re-reads with coincident end caps.
Modelling
- Spline lofts (
ops/loftSpline.js): - Station data that steps (a ply drop) becomes a linear ramp on the smooth cubic, instead of a cubic that rings through thin walls. This is reported in
analytic.vFit(loft_spline_v_steps). - A G1-forced knee break whose fit bulges off its data is released to a corner. This is reported in
sectionFit.g1Released(loft_spline_g1_bulge_released). - An auto corner that fades along the loft is carried to every station, by vertex index or by segment position, instead of refusing
loft_spline_segment_count_mismatch. This is reported insectionFit.cornerUnion(loft_spline_corner_union). - Smooth sections cut into faces by caller breaks keep G1 at the breaks.
- Lofts that none of these apply to are byte-identical.
- Glazing
hostMode:'cut'on a planar host cuts with an exact prism. On a box test it was refused at everyfitTolbut 4 and 5; it now builds at everyfitTolwith the exact pane and host volumes. AfitTolabove 0.1 is now only a rescue. New refusals:glazing_cutter_failedandglazing_cutter_overlap. The cutters are listed inreport.cuts[].
Tools and analysis
cad_dfm_checkdeclaresmachiningStockMmfor cold spray, metal jetting, MoldJet and metal DLP. Before, the closed schema rejected it.- Metal printers: 18 LPBF/EBM printer records gain vendor-sourced build volumes. Four records with no findable source stay null.
- Refusal remedies:
- 36 rows for the
cad_split_*refusal vocabularies. - Rows for
fastener-proud-head-on-omlandmesh_to_brep_timeout. - A row for
freeform_sheet_not_closed. - Self-update:
update:"now"now bypasses staging, channel and the skipped list as documented. Before, the install gate refused on the rollout before the bypass was reached. A halt still blocks.
Fixed reported issues
update:"now"could not bypass staging, channel or the skipped list, because the install gate checked the rollout first.- A second planar lug on a revolved shell was fixed before 0.25.0, but a tessellator change in 0.25.0 left the first union not watertight. That union is now exact and watertight again.
- The second general-engine union on a duct shell no longer fails. The fix shipped before 0.25.0; a regression test landed in this range.
NativeCAD 0.25.x
v0.25.0 “Glover”
Creo parts open as exact solids, and CATIA V5 parts and products, Inventor assemblies, JT and Rhino .3dm files import. SolidWorks parts are checked against their own display lists. Extrude and pad gain end conditions and taper, sketches keep their constraints, cad_wind_tunnel gains a compressible regime, and a strict STEP export is gated on B-rep validity. 185 tools, one more than 0.24.0 (cad_turbomachine). The command schema moves from 1.65.0 to 1.68.0. 1.66.0 adds wind_tunnel regime + compressible, 1.67.0 adds extrude extent / operation / of and pad extent, and 1.68.0 adds sketch_end constraints. Every document valid at 1.65.0 validates unchanged and builds the same geometry. The whole-corpus census opened 15,287 of 17,120 user-facing files (89.3 %). A later full pass at 27bbf90c counts 16,962 open and 462 failing.
Importers (cad_import)
- Creo / Pro/ENGINEER parts open as exact solids (
packages/interop/src/importers/CreoBrep.js,CreoRecords.js). The B-rep is rebuilt from PTC's ownVisibGeomrecords, in both the text and the binary encodings. The test holds a fixed list of NIST parts to their STEP twins within 0.5 % volume and bounding box. Curve-definedtab_cyl/ruled_srf/srf_revsurfaces and the rolling-ballfillet_srfare decoded.PRT_CREO_UNSUPPORTEDchanges meaning: it now refuses only a part whose B-rep does not build, and it carries aCREO_*subCode such asCREO_SHELL_INCONSISTENT,CREO_SHELL_OPEN,CREO_TOPOLOGY_BROKEN,CREO_TRUNCATED,CREO_RECORDS_UNDECODED,CREO_NO_SOLID_GEOMETRY,CREO_NO_VISIBLE_GEOMETRY,CREO_NO_DISPLAY_DATA,CREO_UNKNOWN_CONTAINER,CREO_ASSEMBLY_NO_GEOMETRYorCREO_SURFACE_UNSUPPORTED_FILLET_SRF. - CATIA V5
.CATPartopens as a display mesh (importers/CatiaV5Import.js,importers/CatiaCgrDisplayMesh.js). The embedded CGR scene graph is decoded, and the result issource:'display-mesh',brep:false. 247 of 262 CATParts with display data open (204 closed). The 19 NIST CATParts with STEP twins match the bounding box exactly or within 0.023 % of the diagonal, with a median volume difference of 0.024 %. CGR vertex layout 2 now decodes as well. Multi-segment streams are read with the correct 20-byte segment entries. New codes:CATPART_BREP_CGM,CATPART_DISPLAY_NO_TESSELLATION,CATPART_DISPLAY_UNDECODED,CATPART_NO_DISPLAY_DATA,CATIA_TRUNCATEDandCATIA_UNKNOWN_CONTAINER. - Assemblies open with their components placed:
- Inventor
.iam(importers/IamImport.js) and CATIA.CATProduct(importers/CatProductImport.js) are new.searchPathsis now also accepted for.iamand.catproduct. New codes:IAM_COMPONENTS_MISSING,IAM_COMPONENTS_UNREADABLE,IAM_COMPONENT_NO_BODY,IAM_CYCLE,IAM_NOT_COMPOUND_FILE,IAM_NO_COMPONENT_REFS,IAM_NO_RSE_STORAGE,IAM_NO_SOLID,IAM_OCCURRENCE_MAP_UNDECODED,IAM_TOO_DEEP,CATPRODUCT_COMPONENTS_MISSING,CATPRODUCT_COMPONENTS_UNREADABLE,CATPRODUCT_NO_GEOMETRYandCATPRODUCT_STRUCTURE_UNDECODED. - A
.sldasmwith some components missing now opens as a partial assembly. Before, it was refused. The missing parts are listed onimported.assembly.missingComponentsand in anXA_COMPONENTS_MISSINGwarning.XA_COMPONENTS_MISSINGnow refuses only when no component resolves. - Components missing on disk are taken from the assembly's embedded
ImportedComp/VirtualCompdocuments. Across 452 corpus SLDASM files: ok 71 -> 85,XA_COMPONENTS_MISSING367 -> 351, solids 325 -> 1939.storedFallback.reason: 'XA_STORED_UNPLACED'names a stored fallback. - A component that resolves but does not build makes the result partial with
XA_COMPONENTS_UNBUILT. An assembly box larger than its placed components givesXA_ENVELOPE_SHORT. Later fixes take partial results from 47 to 45. - Siemens JT and Rhino
.3dmare new formats: - JT (
importers/JtImport.js,XzDecoder.js): XT B-rep segments become exact solids through the Parasolid reader. zlib and LZMA/.xzblocks both decode. JT 9.x TopoMesh display LODs decode as well. New codes:JT_BAD_TOC,JT_NOT_JT,JT_NO_BREP,JT_NO_GEOMETRY,JT_SEGMENT_CORRUPT,JT_TESSELLATION_UNDECODED,JT_TRUNCATED,JT_VERSION_UNSUPPORTED,JT_XT_NOT_FOUND,JT_XT_UNREADABLE,XZ_BAD_HEADER,XZ_BAD_MAGIC,XZ_FILTER_UNSUPPORTED,LZMA_BAD_DATA,LZMA_BAD_PROPSandLZMA_TRUNCATED. A stand-in.jtis now refused at build time asE_GEOMETRY/JT_TRUNCATED, no longerE_INVALID_ARGSat validate time. - Rhino
.3dm(importers/Rhino3dmImport.js):ON_Brepand extrusions import as exact B-rep, closed ones as solids. Meshes import as meshes, and the units come from the settings table. New codes:RHINO_NOT_3DM,RHINO_LEGACY_V1,RHINO_LEGACY_V2_GEOMETRY,RHINO_NO_GEOMETRY,RHINO_SUBD_UNSUPPORTED,RHINO_TRUNCATED,RHINO_BAD_BREP,RHINO_BAD_CHUNK,RHINO_BAD_CURVE,RHINO_BAD_MESH,RHINO_BAD_SURFACE,RHINO_FACES_DROPPED,RHINO_UNSUPPORTED_CURVE,RHINO_UNSUPPORTED_SURFACEandRHINO_ERROR. - SolidWorks
.sldprt: - A part with no Parasolid body now opens as its display mesh (
source:'display-mesh',brep:false) instead of refusing (importers/SldprtDisplayMesh.js).import {displayMesh:false}keeps the refusal. On 370 parts with both a B-rep and a display mesh, the mesh bounding box is within 0.5 % on 94 % of parts and the volume within 1 % on 83 %. "STL Graphics" feature meshes open the same way. New codes:SLDPRT_DISPLAY_NO_TESSELLATION,SLDPRT_DISPLAY_NO_GRAPHICSandSLDPRT_DISPLAY_TOO_LARGE. - SolidWorks 2001-2004 PKWARE-imploded streams decode (
DclExplode.js). Body-less parts refuse with the new sub-codesSLDPRT_EMPTY_PART,SLDPRT_EMPTY_PART_GRAPHICSandSLDPRT_SKETCH_ONLY. All 4,874 corpus sldprt were re-imported with zero regressions. - The import is held to the part's own display list. A stale display list warns
stale-display-list, and a disagreeing one warnsdisplay-mismatch; neither refuses. Partition bodies the current display list does not draw are dropped (:DISPLAY_LIST_ABSENT_BODY,DISPLAY_LIST_HIDDEN_BODY). The partition outranks a larger body-store snapshot. Without aconfigurationargument, an import that disagrees with the display list now tries the other configurations and picks the one whose extent fills the drawn box. A fallback stream that disagrees with a current display list is flaggedfallbackDisplayMismatch(SLDPRT_FALLBACK_DISPLAY_MISMATCH). Bodies drawn but missing from the partition are recovered from the body store (5 parts, 49 bodies). - Parasolid
.x_t/.x_b(XtReader.js,importers/ParasolidImport.js): - Old schemas (SCH_1012 to SCH_11xxx and pre-v6) read: 124 of 131 refused files now import, 109 of them watertight. The pre-v6 B-surface and B-curve families are added, taking x_t ok from 143 to 145.
- Periodic NURBS stored with the full knot vector are seamed (
xtPeriodicSeam.js). Full-knot sldprt rows that open go from 34 to 44, andXT_PERIODIC_FACE_ACROSS_SEAMfrom 32 to 22. New sub-codes:XT_PERIODIC_FACE_ACROSS_SEAMandXT_PERIODIC_SEAM_NOT_CLOSED(underXT_SEAMED_FACE), andXT_PERIODIC_CURVE_NOT_CLOSED(underXT_BAD_PARAMETERS). Analytic cylinder, cone and torus faces that use their seam edge twice are kept whole or split. - Blend surfaces on cones, swept, spun and offset supports build. Blend extensions now have an evaluation budget and a 60 s wall-clock budget: Xbox goes from over 150 s to 22 s (a named refusal), and Black_Decker from over 150 s to 32 s.
- STEP:
- Sheet-only files sew into solids (
ops/sheetSolidify.js): 30 of 100 sheet-only STEP files open. New codes:STEP_SHEET_CLOSED_NOT_WATERTIGHTandSTEP_SHEET_NEAR_CLOSED. A file too large for the heap now refusesSTEP_TOO_LARGE_FOR_HEAPinstead of aborting. With this release, 17 of 21 STEP sheet rows publish solids (0 before). - Curve-only STEP, IGES and
.3dmfiles open as wire bodies, and AP242 tessellated-only files open as a mesh body (importers/wireBodies.js). Before, these were refusedSTEP_WIREFRAME_ONLY,STEP_NO_BODY_ENTITY,IGES_NO_GEOMETRYorRHINO_NO_GEOMETRY. - PMI, validation properties, layers, supplemental geometry and saved views are stored on the imported model.
DRAUGHTING_PRE_DEFINED_COLOURresolves to RGB. A secondbuildBRep()no longer resurrects a withdrawn solid. - IGES (
packages/interop/src/IgesReader.js): - The
importcommand publishes the closed lumps of a mixed-lump file as solids and the open rest as one sheet,IGES_STITCH_PARTIAL. A pinnedstitchTolorCADSOLVER_NO_IGES_LUMP_SPLIT=1keeps the single open sheet. - Entities 192/194/196/198, 104 forms 2/3 and 118 are read. A 144 that asks for the natural boundary of one of those surfaces refuses
IGES_UNBOUNDED_SURFACE. In the lane's run, 152 more files open (642 -> 794 of 980). - Curve-only IGES opens as wire bodies by default;
wireframe:falserestoresIGES_NO_GEOMETRY. Entities 112 and 130 are exact. - ACIS SAT/SAB: 79 of 81 harvested files build, from 19. Uniform-scale transforms, subtype refs and pcurves are read.
- Byte coverage: every reader's result and refusal carries a whole-file byte ledger (
byteCoverage, withinModelBytes/droppedBytes).CADSOLVER_NO_BYTE_COVERAGE=1turns it off. - Parasolid (.x_t inside.sldprt) charts: coarse INTERSECTION polylines are re-read against their own edges, so a speaker housing that read +3.4 % over its display list no longer fills its B-spline faces with repair patches; apple/lemon torus faces a few 1e-6 off the outer lobe are no longer refused as trimming into the inner lobe.
Tessellation
cad_export_bodywithmaxTriangleson a single-level export now honours the budget. Before, a blended prism could not go below 13k to 24k triangles.lod.bandAspectRelaxedreports when the second budget stage was needed. An export withoutmaxTrianglesis unchanged.- Imported-solid volumes checked against their display meshes (follow-ups). Examples:
- Keycap pockets: +3.2 % to +6.5 % were fixed.
- Sphere zones meshed as their caps and torus bands were fixed.
- A full-knot helical spring that came out about 27 % thin now reaches at least 950 mm³, against about 984 exact.
- Thread run-outs: open edges 33 -> ≤3.
- Other measured fixes:
- nist_ctc_02 winding conflicts 236 -> 0, now -0.008 % against the file's own
VOLUME_MEASURE. - A P424 R362 skin read 1.86M against 2.33M mm³ at default tolerance; chord 0.1, 0.01 and 0.003 now agree.
- A kinked cubic loft is within 0.01-0.02 % of OCCT.
- Worst sag on a self-crossing ring dropped from 0.694 to 0.0087 mm.
- The weld radius is capped for thin bodies and anchored to the body, not the world origin.
- Faster: unchanged results with less time, from repair, CDT and gate fast paths and from knot-grid refinement budgets (Xbox refinement 27 -> 15 s).
- Rotated tori at coarse chord are watertight and within 8 % of exact; at chord ≥ 7 they were refused
tess_repair_volume_driftat about 12 % of true volume. - Coarse display meshes follow the tolerance (opt-in
coarseDisplay): a 2-face sphere 552 -> 120 triangles, a 3 m x 4 mm pendant 1,556 -> 184. Default meshes are unchanged. - A dense surface-surface intersection trace is no longer collapsed by chain decimation: a strut leg folded into shaft and barrel went from 2,044 facets and a refusal to 18 faces. A stretched-wall union now converges with chord.
Booleans
- Local general booleans (
ops/genBoolLocality.js) chart only the faces that meet the other operand. The P424 main-plane Ø6 hole was refused with 190 gate errors and now builds. The P424 flap builds checkFull-clean; it had 260 errors and took 149 s. The identity check reads B-spline faces against their own surface and imprints into a window of span-wide skins (ops/genBoolWindow.js). A carry backstop that declined every carry now reads like for like. - Curved trims re-authored as B-spline edges (
ops/curvedTrimRecurve.js): 261 LINE chords between a bore and a curved skin become fitted edges (< 3e-5 mm). All 14 P424 main-plane holes cut certified. A second bore no longer inherits chord tolerances. - Impeller blade into hub: the union was refused and now gives 130 faces. The blade-into-hub carry gave 4,557 faces (4,522 PLANE) and now gives 41. An 8-blade chain grows by 13 faces a blade, watertight, with retained heap 422-598 MB. Before, the chain ran out of memory at blade 5.
- Hollow (
BREP_WITH_VOIDS) operands no longer refusenon_positive_lump/non_connected_operandfor a tool inside the cavity. - Tube joints and tube nets (
ops/tubeJoint.js): tube-on-tube unions in the union ladder,tubeJoint: 'weldOn' | 'drilledThrough', non-parallel posts, through-crosses and bar-to-bar joints. A 27-member jungle gym builds in about 1.1 s. New refusals:tube_joint_bar_crossandtube_joint_no_seam_room. unionorder:"overlap"(opt-in): a 50-body frame goes from 31-40 s to about 9-12 s.- Now builds:
- A flush-top union of two bored blocks, which was refused
unsupported_overlap. - A
tube_sweep∪ hub, which was rolled backnon_manifold_result. - Radial cylinder-into-cylinder drills stay on the analytic rungs instead of refusing
cylcyl_base_not_coaxial. - A larger coaxial bore cut as a second subtract.
- Dense teardrop helix subtracts.
- A T-junction bore through a cross-pin bore.
- Pierce no longer depends on position: verdicts are identical at x = 0, 1e3 and 1e5.
- A genBool refusal reaches the caller:
fallbackReasonis never empty and namesops/genBool.jsand its code. - Kernel API: a multi-lump union's id is a lump set (
ops/lumpSet.js). Kernel subtract, union and intersect take the set, or an array of ids, as every lump. A stale set refusesmulti_lump_operand_stale. The document executor still addresses lumps itself (lumpSet:false). - Small drills: an imprint that is not on the crossed face now refuses
IMPRINT_UNSUPPORTED; before, it passed on NaN. Ø0.3 and Ø0.1 drills in inch-numbered blocks build. - More late fixes:
- A near-flush union rung (
ops/flushSnap.js) snaps the tool onto the base plane when every other rung refuses. - One-break closed spline-loft sections take through-cylinder subtract, union and intersect at every break index; 11 of 40 failed.
- A real intersect result smaller than the volume identity's noise floor publishes behind an independent certificate instead of refusing
bool_identity_violation. - Carried plane rims go back on their circles: exact elbow volume. The pierce-swapped rung declines early on an edge in a curved face's split plane.
- A genuinely under-removed cut that the base-spread allowance forgave is now refused
boolean_under_removal.
Modelling
- Extrude and pad end conditions (
ops/extrudeExtent.js, schema 1.67.0): extent {type: blind | symmetric | two_sides | through_all | up_to_next | up_to_face | up_to_body, depth2, face, body, offset, taper, taper2, flip}.operation: new | join | cut | intersectwithof, whose results equal the separate booleans.padgainsup_to_*extents. A taper builds exact PLANE and CONICAL_SURFACE walls.- New refusals:
EXTRUDE_TAPER_COLLAPSE,EXTRUDE_TAPER_SHARP_ARC_CORNER,EXTRUDE_TAPER_CURVE_UNSUPPORTED,EXTRUDE_UPTO_FACE_NOT_PLANAR,EXTRUDE_UPTO_FACE_NOT_PARALLEL,EXTRUDE_UPTO_FACE_NOT_REACHED,EXTRUDE_UPTO_FACE_NEEDS_BODY,EXTRUDE_UPTO_NEXT_NOT_FOUND,EXTRUDE_UPTO_NEXT_CURVED_FACE,EXTRUDE_UPTO_BODY_NOT_REACHED,EXTRUDE_UPTO_OFFSET_FLIPSandEXTRUDE_THROUGH_ALL_NO_MATERIAL. - New validator codes:
EXTRUDE_EXTENT_FIELD_CONFLICT,EXTRUDE_EXTENT_FIELD_MISSING,EXTRUDE_OPERATION_NEEDS_TARGETandPAD_EXTENT_NEEDS_EXTRUDE. cad_extrude_profileandcad_padare now listed top-level-only:extent's nested shape comes fromcad_help({tool}), and the server still validates the full schema. A document without the new fields writes byte-identical STEP.- Sketch constraints persist (schema 1.68.0):
sketch_endtakesconstraints, the geometric constraints and driving dimensions, solved atsketch_endso that an edited value re-solves on rebuild. New refusals:SKETCH_CONSTRAINT_CONFLICT(naming the constraint),SKETCH_CONSTRAINT_UNSATISFIED,SKETCH_CONSTRAINT_INVALIDandSKETCH_CONSTRAINT_UNSUPPORTED. Unconstrained sketches build byte-identical STEP. - Fields the executor silently dropped are now forwarded or refused by name: loft
vSpans(loft_vspans_invalid,loft_vspans_needs_spline), glazingfitTol(glazing_fit_tol_invalid) and revolve inlinesplines(revolve_inline_splines_invalid,revolve_inline_splines_conflict). mesh_to_breproute"quad"(ops/meshQuadPatch.js,meshQuadLayout.js,meshQuadAnalytic.js): an organic mesh becomes a few-face bicubic B-spline solid. Thequadoptions keep exact planes and cylinders on the call'sinterfaces/cuts. On the lattice-bracket test all 5 bores come back as CYLINDRICAL_SURFACE; before, all 714 faces were B-spline patches.mesh_to_breprefusesmesh_to_brep_timeoutwithin its wall budget.- Blends:
ease_edgesfallback:"partial".- Blanket blends round mixed line/arc chains and shallow saddle rims.
- Skew-cylinder and mixed-chain bands.
- Face blends on cylindrical side faces stay exact cylinders.
- Swapped ellipse-setback corners close.
- Fasteners:
- New kinds:
csk_iso7046,csk_iso10642,csk_raised_iso7047,pan_iso7045andconnector_bolt. - New
drivefield:pozidriv | phillips | hex | torx | none. import {wetted:true}tags a body as OML, and afasteneron it may omitkind(the head defaults tocsk_100).- Turbomachinery:
bladegainsrotor(ops/turbo/rotor.js). Closed (shrouded) impellers build without the shroud boolean, liftingrotor_shroud_unionexcept for closed impellers with splitters (ISO 2858 80-50-200 in about 3.7 s). - Heal and check: heal keeps
BREP_WITH_VOIDSbodies solid and adds anuntanglestage. checkFull reportsplanar_outline_self_intersectingfrom the true curves and dismisses crossings between provably disjoint surfaces. - Glazing reports per-pane folding (
panes[].folded,flippedTriangles,report.foldedPanes) and rebuilds a folding 4-corner pane from its other corner readings. - Lattices:
lattice_fillexchanger ports may be local discs, several to a face. New refusals:tpms_exchanger_port_overlapandtpms_exchanger_port_spacing.- Rib and corner relief no longer leave consumed solids live.
- Spline lofts align section start points: sections starting at different vertices folded the skin (-15 to -68 % volume); start points are re-chosen by least twist and reported in
sectionFit.startAlignment. Descending stacks build outward.
Simulation
cad_wind_tunnelregime:"compressible"(packages/cfd/src/compressible/, schema 1.66.0): a mesh-free compressible solver around a body or an imported mesh. Inputs go incompressible(mach0.3-25,altitudeorpressure+temperature,modeleuler / laminar / SA). The report gives the coefficients, the shock standoff against Billig 1967, andvalidity.knownShortfalls(the Mach 3 cylinder standoff is +7.6 %). New reason codes:compressible-mach-range,compressible-altitude-range,compressible-freestream-conflict,compressible-gamma-range,compressible-model-unsupported,compressible-gpu-unavailable,compressible-cell-budget(E_LIMIT, 2,000,000 cells),compressible-under-resolved,compressible-bad-mesh,compressible-field-not-read,compressible-grid-invalidandcompressible-diverged.- The incompressible tunnel's default numbers move:
- The default upstream padding goes from 1.5 to 3.0 body spans.
- k / ε / ω are transported.
- The far field defaults to
'auto'(slip walls when the box allows). - A voxel is solid when its centre is inside.
- The sphere at Re 5e4 now reads Cd 0.4684 / 0.4910 / 0.4974 / 0.4950 on 5 / 6 / 8 / 12 cells/D, in the 0.40-0.50 band; before, it converged near 0.518. The old behaviour is reachable through
bc.solver(turbulenceTransport:false,farField:'open',voxelFill:'legacy'). cad_fea_caseon a field body (cad_lattice_filloutput"field") is solved in-process by the homogenised lattice path, never meshed whole. New codes:field_body_no_skin(E_PRECONDITION),field_body_source_missing(E_NOT_FOUND) andfield_body_case_unsupported.
Tools
- New tool
cad_turbomachine(ops/turbo/turbomachine.js):kindis pump, inducer, compressor or turbopump; you give aduty, and it returns the meanline, curves or map, checks and a STEP assembly imported to the session.turbineandturbochargerare routed and refusedE_UNSUPPORTEDwithE_NOT_IMPLEMENTED.options.build:falsereturns in milliseconds; a build takes 20-25 s. Seecad_help({topic:"turbomachine"}). cad_design_knowledgeaction:"laminate"(ops/compositeLaminateAnalysis.js): a classical-laminate calculator (thickness, areal mass, A/B/D, first-ply check, variants table), with refusalsE_UNKNOWN_MATERIAL,E_NONPHYSICAL_MATERIALandE_EMPTY_STACKindetails.reason. The tool'sreadOnlyHintis now false. Seecad_help({topic:"composites"}).cad_printmode:"ded"(packages/mcp/src/tools/print/ded.js): writes a robot DED/WAAM program (ABB RAPID, KUKA KRL, FANUC TP, Yaskawa INFORM) and sends nothing. Refusals indetails.code:ded_post_refused,robot_unknown,ded_toolpath_bad_input,ded_toolpath_bad_surface, and theded_toolpath_*/ded_cell_*/ded_post_*families.cad_view_bodyshares five display modes with the other viewers (shaded, edges, wire, hidden-line, x-ray) and ships the B-rep edges.cad_set_part {flow:{flowPath}}turns on the FLOW-AREA rule incad_dfm_checkandcad_design_review. New findings:sudden_expansion,hub_step,strut_forms_throatandthroat_not_at_min.- DFM: the LPBF wall and gap probe no longer reads a small radial bore in a curved rim as a 0.1-0.2 mm wall. The moldability draft remedy sends
cad_draftonly a draftable set. All 48 METAL and foundry rule codes that had no remedy now carry one: 11 mechanical (cad_move_face,cad_support,cad_draft) and 37 honest advisories. cad_print_prepno longer forwardsdocumenttocad_dfm_check, whose closed schema rejected it.- A JSON-string
faceoncad_padis parsed again ({"normal":[0,0,1]}sent as a string was refusedE_GEOMETRY): string-to-object coercion now resolves the deduped schema's shared definitions, for every tool whose schema has them. - Budgets:
cad_mesh_compare/cad_mesh_sectionmove to a newmeasurefamily (16 KiB, 120 s), and the compact report names what it cut indropped.cad_server_infogets a 96 KiB override; it reached 76,086 B and spilled toE_LIMIT.
Verification and export gates
cad_verify {gate}is the pre-handoff gate (ops/handoffGate.js). It runs interference, coincident faces, fasteners, OML, mechanism poses and claims checks, and returns a signed report; a verdict other thanpassis a hard fail. Inside the samegateobject:gate.stepsadds the outgoing STEPbrepclass.rules.mesheradds a gmsh + CalculiXmesherclass.- A cost-gated visual rule is included.
- A strict
cad_export_bodySTEP is gated, where strict meansstrict:true, a handoff-marked part or a handoff document: - Before writing, a mesh-derived face refuses
E_GEOMETRY/export_mesh_derived_faceswith every face listed. - After writing, a no-heal re-import that fails moves the file to
<path>.brep-rejectedand refusesE_GEOMETRY/export_brep_invalid.CADSOLVER_NO_EXPORT_BREP_GATE=1turns it off. - A non-strict export only warns.
- The independent OCCT oracle runs on cadquery-ocp 8.x as well as 7.x; on 8.x it crashed on an import, so the OCCT half never ran and every strict export warned that it had not.
- Face provenance:
cad_body_topologygives each face'sorigin(exact, healed, genbool_facet, mesh_clip, mesh_to_brep or imported) and afaceOriginscount withmeshDerived. - Assembly checks:
- The fastener audit reads prismatic (flat / D) pins along their extrusion axis.
- Findings name the second body in
nut,blockedBy,host/hostsandnearestFastener. - The OML check measures against the enclosed volume, not the skin bodies.
- Every clashing pair carries an
overlapRegion.
Retired codes
sketch-solve-degenerate-lineis retired. A two-whole-line coincident forces the not-parallel distance instead.
Packaging
- Cursor edition and a stable npm alias:
nativecad-cursor.zipandnativecad-mcp-latest.tgzare listed under the release signature inlatest.json. Both are notify-only for self-update. - Kernel bundles: the three
@nativecad/kernelbundles share one copy of every module, and the index loads op modules and data tables lazily. The staged kernel now carriesops/turboflange data and the latticekt_skintable. Before, they were missed silently through dynamic orfsreads. - tools/list: the new
extent/operationfields took the listing to 405,487 B against the 405,504 B line. Publishingcad_extrude_profileandcad_padtop-level-only brought it to 403,455 B. - Improvements to the bug feedback loop and gathering. IP protections and checks enhanced.
Fixed reported issues
cad_fastenerthrough pre-drilled holes published the kept plates as deleted bodies. The executor now retires only inputs that were replaced.- Regression tests landed in this range (
tests/test_cadsb-100429.js,tests/test_cadsb-100300.js). The code fix shipped before 0.24.0. TRIMMED_CURVEPARAMETER_VALUEtrims. The issue does not reproduce on master, because an earlier change already fixed it. A 160-case regression matrix landed; no reader change.
NativeCAD 0.24.x
v0.24.0 “Gelmetti”
NX .prt, Inventor .ipt and SolidWorks .sldasm files import, the tangent boolean cases build exact or are refused by name, and a declared mechanism can be checked across its whole motion. 184 tools, one more than 0.23.3 (cad_assembly_sweep). The command schema moves from 1.64.0 to 1.65.0: import gains an optional searchPaths for .sldasm components, and every document valid at 1.64.0 validates unchanged. The import census over the 654-file corpus stands at 437/654 (66.8 %) on master af70d847, from 306/654 earlier.
Importers (cad_import)
- NX
.prtparts open through their embedded Parasolid partition (packages/interop/src/importers/NxPrtImport.js). 21 of the 25 NX corpus parts import; 13 STEP pairs agree within 0.03 % volume (ftc_11 0.103 %). Creo parts are refused by name (PRT_CREO_UNSUPPORTED), as arePRT_TRUNCATEDandPRT_NX_ASSEMBLY_NO_BODY. The validator accepts.prtand.prt.N;XbDecoderreads the FEATURE (90), MEMBER_OF_FEATURE (91) and TRANSFORM (100) layouts. - Inventor
.iptparts open through their embedded ACIS SAB stream (importers/IptImport.js,importers/SabDecoder.js), transcoded to SAT and read bySatReader.js. Container faults refuseIPT_NOT_COMPOUND_FILE,IPT_NO_RSE_STORAGE,IPT_NO_BREP_SEGMENT,IPT_SEGMENT_CORRUPTorIPT_SHAPEMANAGER_UNSUPPORTED. With the SatReader extensions below, the ipt census is 16/18; the 2 left are hand-made placeholder fixtures (a 46-byte.ipt, a 50-byte.iam). SatReaderreads more of ACIS:{ref N}subtype back-references, exact /rb_blend/ sweepspl_surthrough their saved B-spline approximation, intersection curves from their B-spline approximation, reversed surfaces and intcurves, reversed narrowing cones, inward-normal sense flips, apex vertex loops, periodic closed curves and elliptical cylinders.nullbs, elliptical cones and dangling refs keep named refusals (SAT_UNSUPPORTED_SURFACE/SAT_UNSUPPORTED_CURVE).SAT_CONE_HALF_ANGLEis retired.- SolidWorks assemblies open from their referenced parts (
importers/SldasmComponents.js,importers/SldasmImport.js). The importer readsswXmlContents/COMPINSTANCETREE, decodes each instance'sswTransform(checked againstswBoundingBoxon 115/117 assemblies), flattens sub-assemblies, and imports each component.sldprtthroughParasolidImport. Components are looked for in the assembly's folder and in the newsearchPaths(1 to 64 folders, searched recursively,.sldasmonly). A reference that cannot be resolved refusesXA_COMPONENTS_MISSINGwith the missing files and the folders searched. An assembly that stores only open sheets falls back to its resolved components when every one resolves (BerringForSunGear 0 -> 13 solids, TransWSyncro 0 -> 221), else reportscomponentFallbackwith the missing list. sldasm census 3/100 -> 82/100. .sldprtconfigurations:importtakesconfiguration("Config-<n>", a name, or a zero-based index); the result'simported.configurationsays which one was imported and lists the others. An unknown selector refusesSLDPRT_CONFIGURATION_NOT_FOUND. x_b deltas streams resolve to their base partition (66 files, 0 -> 37 solids), and a rollback-tail fingerprint resolves 19 of the 36 ambiguous ones, 0 wrong.- IGES (
packages/interop/src/IgesReader.js): - 308/408 subfigure instancing: tachyon 167 -> 798 closed bodies, boron 93 -> 259 solids. Closed pre-sewn bodies whose report mesh is refused are published as solids with a named defect in
imported.meshRefusals[]; Teensy imports as 176 solids + 1 named refusal. - The whole-file sew measures exact curve deviation for edge pairs that share welded vertices but fail the control-polygon check (
ops/stitch.js). 8 of 19IGES_STITCH_OPENcensus files, plus OLED, now close with STEP-matching volumes. Genuine open sheets refuseIGES_SHEET_OPEN, and 4-way edge coincidencesIGES_STITCH_AMBIGUOUS_EDGES. - MSBO B-rep support (
IGES_NO_GEOMETRY38 -> 19 of 647), each MSBO body sewn on its own with micro-edge collapse below file resolution, and MSBO edges bridged from a 502 vertex to a 504 curve end within file resolution. The import command reuses the per-body sew (boron 93, xenon 45, argon 49 solids). - Entities 143/141 bounded surfaces, 104 ellipse arcs, 114 surfaces and the 126 curve range;
STITCH_FAILED18 -> 0, closed files 482 -> 491 of 647; B523 orientation 73 s -> 7.7 s. - Parasolid
.x_t/.x_b(XtReader.js,importers/ParasolidImport.js): - Git LFS pointers refuse
XT_GIT_LFS_POINTER(with size, oid and thegit lfs pullremedy), and header-only transmit stubs refuseXT_NO_BODY_DATAinstead ofXT_HEADER_MALFORMED/XT_ENCODING_UNKNOWN/XB_BAD_RECORD. The sniff flagged only 5 corpus stubs, 1 fixture and 1 empty file across 19,389 small x_t/x_b files. - Charted blend spines map
u = base_parameter + base_scale * s(end caps were up to 8.7 mm off). TRIMMED_CURVE windows are sampled param_1 -> param_2, so sense-is no longer reversed. Apple-torus inner-lobe faces build, whole-sphere/torus SPUN_SURF faces build and leading wire/point bodies are skipped. - Closed XT sheets sew to solids; a genuine sheet source gets the
SHEET_BODY_SOURCEoutcome. - PDF drawings on one annotation-named layer import (
ops/pdfDrawingImport.js). When every curve sits on a layer named like annotation ("Dims"), the layer name is ignored and the stroke style decides: the wide continuous pen is the view outline, the thin pen is dimension and extension lines. These sheets were refusedPDF_NO_VIEWS.
Booleans: tangent cases
The 19-case tangent table (three operations each against closed forms) pinned six kernel gaps; all six are closed in this release.
- Equal-radius tee, exact subtract and intersect (
ops/cylCylEqualBool.js). The subtract was refused (self_intersection) and the intersect chorded into 87 LINE edges. Now every edge is an exact ELLIPSE, CIRCLE or LINE: subtract 8 faces / 12 edges, intersect 4 faces / 4 edges (V = 8R³/3). - Steinmetz cross, exact subtract and intersect. The general engine had the right volume but 78 / 152 LINE chords where the ellipses belong. Intersect is the bicylinder (4 faces, 4 ELLIPSE edges); subtract is the base rod's two halves as two lumps.
- Nested tangent balls (
ops/nestedTangentBool.js): the union is an exact copy of the outer ball (was 2 faces / 6 edges with straight secant edges), the intersect is the inner ball, and the subtract, which would leave a cavity open through one point, is refusedtangent_contact_pinchwith the contact point. A rod inside a rod touching along a line now leads with the sametangent_contact_pinchverdict. - Near-tangent unions are exact or named, not faceted (
ops/arcPrismUnion.js,ops/booleanClearance.js). Two parallel rods 1e-3 mm into each other unioned to 638 PLANE facets; they now build exactly (4 faces). A ball 1e-6 mm into a block (2189 facets) is now refused as a touch at a POINT, with the point. - No-volume union refusals name the contact (
ops/booleanClearance.js): "touch at a POINT [x,y,z]", "touch along a LINE …" or "are DISJOINT - a GAP of g mm separates them". The reason code is unchanged. - An empty intersect says when it dropped a sub-tolerance sliver: a warning with code
sub_tolerance_overlap(dropped volume, area, mean thickness vs the resolution) on the op entry. A true contact gets no warning.cad_assembly_contacts/ clash volumes build the real sliver body instead.
Booleans: other fixes and speed
- Welded frames:
- A
tube_sweep{points, bendRadius}round tube now builds as exact cylinders + torus bends by default (surfaces: "analytic"), and a union that meets one of its straight legs stays on the analytic rungs (ops/tubeLegSplice.js).bendRadiiis the per-corner spelling. The same path withoutbendRadiusis unchanged. - The ball-jointed elbow (
ops/ballElbow.js: two equal tubes ending at one node plus a ball of the tube radius) builds exactly in every operand order (≤ 9 faces). It was refused by genBool's validity gate or asnon_connected_result. cad_boolean_union {mitre:true}(ops/frameMitre.js) is the welded mitre joint of two straight round or box members: one planar seam, V = section area × the two axis lengths. Unequal sections refusemitre_sections_differ.- Faster large booleans (-100495, -100496, -100497, -100499, -100505, -100513, -100525, -100537, -100545). The braid ∪ ring union runs end to end in about 135 s, was over 180 s. This release has
pierce()query the base through a solid probe instead of tessellating it whole. This release gives genBool's planar imprint scan a BVH broad phase that leaves results bit-for-bit identical. pierce()refuses past 32 distinct pierced base planes (non_parallel_pierce) and hands the pair to the general engine. A plane-faceted braid ∪ annular prism used to run 130 s+ with no answer. A planar tool end flush with the entry face, on a tool that cleanly pierces the opposite face, is cut as a through hole (box Ø8 3590 -> 317 ms).- Coplanar merge accepts a near-miss only when exact B-rep volumes agree (the intersect, 200 -> 4 faces). Tangent-point cusps collapse exactly (
ops/tangentCuspCollapse.js). The maximal-face pass no longer leaves faces bounded by chords that are off their own surface (ops/faceMerge.js). A drill-tip cross bore followed by a second one builds (ops/faceSplit.js). - Feature-pattern folds and sequential holes reuse their meshes (
ops/featurePattern.js,ops/incrementalCertificate.js): a hole pattern passes one SolidProbe face cache through every fold instead of re-stamping every triangle per fold, and the boolean result gate no longer re-meshes the hole-carrying faces on every operation. - The Bambu
.3mfwriter instances shared meshes: a plate of repeated parts is written once per mesh plus a transform per copy (x1c-plate.3mf27.7 MB -> 1.9 MB).
Features
cad_hole_patternkind"radial"(ops/radialHolePattern.js): rows of radial through-holes in the coaxial walls of a tube or tube-in-a-tube in one pass,{kind:"radial", rows:[{station, count, diameter?, startAngle?, stagger?, walls?}], axis?, origin?}. This is a mode of the existing tool, not a new one;radial_hole_patternis also acad_build_planverb. 240 holes in one op take 12.0 s with no genBool call, volume within 2e-7 of the closed form. Circularfeature_patternradial seeds and polar hole rows route onto it (CADSOLVER_RADIAL_ROUTE=0turns that off).cad_chamfer_edgesgainsflip: it swaps which support face takesdistanceand which the second setback (distance2orangle). The result reportsflipped. Flip on an equal-distance chamfer is refusedchamfer-asymmetric-unsupported.- A blanket chamfer refuses an input that already self-intersects (
chamfer-input-invalid), the twin of the fillet'sfillet-input-invalid. - Shell (
ops/shellCurved.js,ops/nurbsOffset.js): exact cylinder/plane tip chords and cone/plane counterbore corners shell watertight; a box with one 1 mm fillet shells at t = 2; a rational patch whose arc weight drifts meets its tolerance through a homogeneous refit. Shell refusals carry their cause as data (collapseCause,minRadiusRatio,deviationRatio). - Blend refusals carry
reach: keep-edge / keep-surface refusals report the overrun and the largest radius that fits. Elliptic and B-spline rim pairs are classified. - Glazing: a closed-loop member (A, B, C, D, A) gives pane edge D-A the closing span; it was refused
glazing_pane_too_small. cad_engrave_textwithface:{normal}honoursorigin: among same-normal faces it picks the one that containsorigin, not the one furthest along the normal.
Assemblies and fasteners
- New tool
cad_assembly_sweep(packages/assembly/src/Mechanism.js,MechanismSweep.js). Declare a mechanism once: revolute/prismatic joints, loop closures, one driver with its range, end stops and named poses. Per body pair in relative motion it returns the minimum clearance over the range (negative = penetration), where it occurs, clash intervals and the value at each named pose. It also gives the transmission angle range and toggle margin per closure pin. Conservative advancement steps each pair by its clearance over its maximum relative speed, so a 2° latch hit that an 11-pose sweep skips is found.checksruns the z-fight, contacts, access and OML checks at every pose;exportwrites both end states and a scrub GLB. It moves nothing in the session. cad_jointkind"pin"(ops/revoluteJoint.js): a real revolute stack on any axis. It has an ISO 3547 PTFE-lined bush in the moving part, an ISO 286 journal fit (default H8/f7), PTFE thrust washers, ISO 7089 washers, an ISO 4017 bolt and an ISO 4032 nut.joint.sweepdrops straight intocad_assembly_sweep. A partB that does not straddle the pin is refusedjoint-pin-needs-fork.cad_fastenerseats hardware inhostbodies by envelope (ops/fastenerHost.js). The clearance hole, countersink or counterbore and a pottedinsertpocket are cut in every host or in none. Failures are named:fastener-host-cut-failed,fastener-host-cut-incomplete, andfastener-flange-wider-than-wallwith the largest Ø that fits.wetted:true(an OML face) makes the hardware a flushcsk_100head, and a proud head is refusedfastener-proud-head-on-oml.cad_fastener_auditmeasures along the hardware against the modelled solids (ops/fastenerSeat.js), not only with radial rays. New findings:shank-in-solid,head-coplanar-uncut,insert-inside-host,flange-breaks-outandnut-unreachable. Imported hardware with no placement record (hardware,inserts) is read off its own solid. A body it cannot read is listed instats.notAuditedand makes the audit incomplete.cad_assembly_contacts: facing coaxial cones (a countersunk head in its seat) report their gap incontacts.conical. A bore/shaft clearance ≤ 0.05 mm radial, or a declaredfits.runningFitspair, is a running fit and counts as joined;fits.pressFitsmarks designed interference.coincident:trueadds the z-fight check (same-facing coincident area per body pair), andfiletakes a.glb.cad_design_review kind:"assembly"reports the same check as advisoryassembly_coincident_facesfindings.- The assembly check bounds its exact overlap-volume pass (4 s per pair, 12 s per check, then ray sampling named
boolean-budget-exceeded). A glazingcad_design_reviewwent from 158 s to 42 s with identical volumes.
Output, printing and casting
- A cold whole-document
cad_preview_body, or a tessellatingcad_export_body, estimated past the job cost line returns ajobIdinstead of blocking past the 60 s bridge wait (packages/mcp/src/tools/output/cold-cost-route.js). The estimate is 0.04 s per face, against the shared ~40 s line (CADSOLVER_MCP_JOB_COST_S). A document already tessellated in this process stays inline, and formats that write the B-rep as is (STEP, x_t, SAT) are not routed. - Tessellation: the watertight repair guard judges a closed but non-manifold or inside-out mesh against the B-rep volume. A repair that lands within 1 % of it is accepted; before, it was refused
tess_repair_volume_drift. An explicitchordToleranceno longer meshes a small circle coarser than the default. Sliver faces no longer fold. cad_nestgains thesls_eos_p396target andwallMarginMm.cad_cast_mouldgainsdraftAllowance("auto"default,"off", or degrees) on 0° moulded walls (ops/draftAllowance.js). Pour-along-pull orientation, the spigot boss as a cavity-trimmed pillar, and the gate bore and boss cut before the sheet split.- The published kernel ships every printer record, so a printer such as
x1cresolves from the bundle. - A resumed
cad_cast_mouldanswers inline again. The report carried the placed nest's per-copy layout (nest.placed.objects,items,placedBoxes: 111 KB for a 40 x 30 x 20 mm box), which took the response to 253 KB, past the 128 KiBreportbudget, so it was spilled to a file.report.nest.placednow keeps the summary withobjectCountanditemCount; the build 3MF it names holds the full layout (0.24.0 release,ops/castMould.js).
Licensing and packaging
- The MCP server refuses an SDK development key.
cad_licenseanswersE_INVALID_ARGSwithdetails.reason: "dev_key_not_mcp"before applying it. A dev key found at boot is dropped with a warning and the server runs unregistered. Development keys license the NativeCAD SDK only (Development Licence Agreement §2.3A). - The Cowork
.pluginno longer shipsbin/: claude.ai rejects plugins with a top-levelbin/, and.mcp.jsonlaunchesdist/server.min.jsdirectly. - The tools/list line moves from 393 KiB to 396 KiB (402432 -> 405504 B):
cad_assembly_sweepand the new import, fastener and joint arguments took the public listing to 402901 B across 184 tools. The private fea-intact line follows at +5 KiB (401 KiB) and the absolute ceiling at 403 KiB (packages/mcp/scripts/measure-tool-list.js). cad_assembly_sweeppublishes a wiretypeon its three union fields (checks.poses,mechanism.stops[].at,sampling), so a schema-driven client sends them structured (audit).- A CycloneDX SBOM for this version is committed at
deploy/sbom/nativecad-mcp-0.24.0.cdx.json(327 components).
Showcase pages (tangent booleans, transaxle fastener checks, 3D nesting) and the import census are repository content, not part of the bundle.
Fixed reported issues
- A PDF drawing with everything on one annotation-named layer was refused
PDF_NO_VIEWS. cad_engrave_textface:{normal}ignoredoriginamong same-normal faces.- Cold
cad_preview_body {of:"all"}(116 s) and GLBcad_export_body(71 s) on a 23-body STEP assembly timed out past the 60 s bridge wait.
NativeCAD 0.23.x
v0.23.3 “Gatti”
Spur gears are exact B-spline solids, angled and face-placed features export to .FCStd as PartDesign features, and the hollow bent pipe + second holed flange union no longer cracks. 183 tools, one more than 0.23.2 (cad_cast_mould). The command schema version is unchanged (1.64.0); cad_dfm_check gains the foundrylab_dmc process and its rules. The three headline fixes come from the cadbench.ai rerun of 0.23.2.
Gears (cad_gear)
- Spur gear flanks are exact cubic B-splines in the kernel (
ops/gearExact.js,ops/gear.js). A 50-tooth gear is 202 B-rep faces (was 2802) and its STEP file carries B-spline surfaces instead of faceted flanks. - The
.FCStdexport of a spur gear is a blank Pad, then a tooth-gap sketch with 2 B-splines, a Pocket and a PolarPattern, so FreeCAD rebuilds the teeth as features: recompute gives 205 faces and the same volume. Editing the tooth count or module on the exported file is follow-up work, not in this release.
FreeCAD export (cad_export_body {format:"fcstd"})
- A face-placed
cad_hole_patternexports as a Hole plus aPolarPatternorLinearPattern. The row carries the depth every instance was cut to, so a through row on an angled face stops at the far side of the wall. - A rotated box or cylinder exports as a sketch plus a Pad on a placed sketch plane. Nothing in the cadbench elbow falls back to a
Part::Featuresnapshot.
Booleans
- Arcs that are pieces of one full circle share that circle's 64-point spacing (
ops/faceSplit.js,ops/genBool.js,ops/faceMerge.js). A ring split as four quarter arcs now matches one split as two halves, which is what cracked the hollow bent pipe + second holed flange union (181 open edges). The union is one watertight solid, andfaceMergerestores true circles at the leg and bend joins. - Separate
cad_boolean_unioncalls work one after another (follow-up,commands/rebuild.js,ops/revolveTrim.js,ops/splitSurface.js). A parser-bound entity left on the parser by the revolve trim and surface split was copied onto the next call's fresh parser, so the second union (pipe ∪ flange, then ∪ the second flange) wrote into the old document and failed with an unknown solid. The rebuild now drops it, and each call starts clean.
Casting moulds
- New tool
cad_cast_mould(ops/castMould.js): one call from a part to a printable two-half casting mould with a JSON + HTML report. The linear shrink allowance is required. It also degrades to a partial result after import instead of failing, adds the spigot boss and funnel (the boss foot is now checked for contact with each half's top and extended as a pillar when it would float,ops/spigotContact.js), nests mould pairs in the build volume and builds the shell from scan-compensated geometry. - Mould halves carry more of the casting: the conformal flange and its foot are fused onto the shell before the split, so the cavity is cut once (
ops/mouldFeatures.js,ops/mouldShell.js); planned core prints get seats and the extra risers are cut into both halves (ops/mouldSeats.js). - Mould export is interoperable (
ops/mouldInterop.js): per-half STEP files withhalf_A/half_Bproducts, a<part>_mould_setSTEP in AP214 and AP242 with the halves, cores and parting surface, and a<part>_mould_set.3mfwith named objects.ops/mouldInterop.jsnow loads its helpers with literal requires, so the bundled server carries them (the release build refused its dynamicrequire). - The automation rate separates hand-finishing work from review advisories: each remaining item carries
kind: "work"or"review", andfullautomation means no work items left (review advisories allowed). cad_dfm_check pack:"foundrylab_dmc"(ops/foundryLabRules.js): the Foundry Lab digital-casting rules. A call withoutshrinkPctis refused withFOUNDRYLAB_SHRINK_REQUIRED.
Other fixes
cad_picogkrefuses to import a decimated body with non-manifold edges. Decimation first splits pinched edges; if any survive, the call answersE_GEOMETRYwith codePICOGK_DECIMATE_NONMANIFOLDand namesimport:falseor a largermaxTriangles. Open edges alone are still a warning.cad_assembly_contactsno longer reports a bore/shaft interference when the radial material does not overlap.
Fixed reported issues
- False bore/shaft interference in
cad_assembly_contacts.
v0.23.2 “Frühbeck de Burgos”
FreeCAD round trips close the cadbench.ai pilot gaps: cad_edit_guarded edits .FCStd files, the .FCStd export maps gears, patterns and sweeps to real PartDesign features, and repeated features steer to one pattern call. 182 tools, unchanged from 0.23.1. The command schema is unchanged (1.64.0). The fixes come from the cadbench.ai pilot, where NativeCAD failed the spur gear (frozen geometry), the two edit items, the lego brick (ran out of turns) and the flange (3 bodies).
Editing .FCStd files
cad_edit_guardedworks on.FCStdfiles (ops/fcstdReader.js,packages/mcp/src/tools/state/edit-guarded.js).inspect:truelists the document's sketches, constraints, features and patterns without changing anything. An edit can set a sketch constraint by index, by name or by{type, value}; set sketch geometry; and change a pattern's occurrences, angle or length. Only the changed element is rewritten and its object is marked Touched, so FreeCAD recomputes exactly what changed.
FreeCAD export (cad_export_body {format:"fcstd"})
- The reply no longer fails with
E_INTERNAL.cad_export_body's outputSchema now declares thefcstdblock, so the file and itsfcstdsummary come back as a normal result. This was the known issue in 0.23.0 and 0.23.1. - A spur gear exports as a full-profile sketch plus a Pad, not a frozen B-rep snapshot, so a grader can edit and recompute it.
- Patterns export as PartDesign features: linear patterns as
LinearPattern, circular patterns asPolarPattern, mirrors asMirrored(pattern,feature_pattern, through-drillhole_patternrows andmirror), and a roundcad_tube_sweepalong a planar bent path asAdditivePipe. Patterns that do not fit stay B-rep snapshots and are listed infcstd.fallbacks. - One PartDesign Body per NativeCAD solid. A boolean never leaves its tool as a second Body: the tool lands in the base Body as features (a primitive, a replayed recipe, a pattern group or
Mirrored), and a tool with no feature form (a filleted chain, anintersecttool) is frozen as a hidden B-rep combined byPartDesign::Booleaninside the base Body. The cadbench elbow flange, 3 Bodies in 0.23.1, is one Body.
Agent guidance
- Repeated features are one pattern call. The MCP instructions gain a "Repeated features" section and the plugin's
SKILL.mda matching step: studs, tubes, ribs, vents, a bolt circle or a hole grid go tocad_hole_pattern,cad_feature_patternorcad_pattern_copies, never one call per feature. - Unattended runs (repo integration, not in the bundle): the Harbor benchmark agent's system prompt says nobody can answer questions, so it takes the best fallback, still writes the output file and says what it compromised.
v0.23.1 “Fedoseyev”
.FCStd files exported by NativeCAD now rebuild when FreeCAD recomputes them. 182 tools, unchanged from 0.23.0. The command schema is unchanged (1.64.0).
FreeCAD export
- Every object in an exported
.FCStdis marked Touched.cad_export_body {format:"fcstd"}writes no stored shapes, and without the mark FreeCAD opened the document as "Up-to-date":recompute()did nothing and every PartDesign Body read as a null shape. Any grader that opens the file and recomputes it (the cadbench FreeCAD validator does) scored 0 geometry on every NativeCAD output. The writer now putsTouched="1"on each<Object>declaration (ops/fcstdWriter.js), so the firstrecompute()rebuilds every sketch and feature. Checked with FreeCAD 1.1.0 on a.FCStdexported by the built bundle: a 40 x 20 x 10 mm pad with a 6 mm hole recomputes to a valid Body of 7717.257 mm³, the NativeCAD volume. Files exported by 0.23.0 still open in FreeCAD; they need Mark to recompute (Ctrl+Shift+R) orobj.touch()on each object beforerecompute().
Other changes since 0.23.0
- cadbench rig (repo tooling, not in the bundle):
pick_representative_shapereturns(shape, owner). - Metal printing DFM:
cad_dfm_checkgains EBM, binder jet, metal FFF, DED/WAAM/EBAM, cold spray, metal jetting, MoldJet and metal DLP rule packs and a metal post-processing chain; its rule codes are listed once as an enum, which shortens its listing. cad_helpmay return up to 128 KiB for one schema answer, so thecad_dfm_checkschema no longer spills.- Mesh to B-rep pre-splits full-turn cylinder walls and imprints cylinder and cone patches.
v0.23.0 “Eschenbach”
NativeCAD writes and reads native FreeCAD 1.1 documents, checks its own work with cad_verify, and edits behind a firewall with cad_edit_guarded. 182 tools: the 180 from 0.22.1 plus cad_verify and cad_edit_guarded. The command schema is unchanged (1.64.0); cad_export_body gains the fcstd format.
FreeCAD on the MCP host. Fillet, chamfer, hole and pattern export to .FCStd, ops that fall back to an embedded B-rep, cad_verify's FreeCAD recompute check, a .FCStd reference in cad_verify, and cad_edit_guarded on an .FCStd need FreeCAD 1.1 on the MCP host, with FREECADCMD set to its FreeCADCmd executable. Without it they return a clear error that names FreeCAD 1.1 and FREECADCMD (code E_FREECAD_MISSING when the executable cannot be started), never a raw spawn ENOENT; cad_verify reports the recompute as not run, with that reason. Pads, pockets, revolves and params export without FreeCAD.
FreeCAD 1.1 writer: cad_export_body format "fcstd"
cad_export_body {format:"fcstd"}writes a native, parametric FreeCAD 1.1 PartDesign document from the whole op history (parent). Each live body becomes a PartDesign Body: fully constrained sketches (0 DoF) with Pad, Pocket, Revolution, Groove, Loft and Pipe (101941, 101942); Fillet, Chamfer, Draft and Thickness with geometric edge and face references, and Holes with ISO thread tables (101958); Linear, Polar, Mirrored and MultiTransform patterns with parameter-bound counts and spacing; and aParamsspreadsheet driving every parameterised field. Changing a spreadsheet value in FreeCAD rebuilds the part.- Nothing is dropped. An op PartDesign cannot express is embedded as a
Part::FeatureB-rep snapshot (joined by aPartDesign::Boolean) and listed infcstd.fallbacks[]with "parametric editability lost here". The response also carriesfcstd.mapped,fcstd.parametric(true when there are no fallbacks),fcstd.paramsandfcstd.ignored. - Whole document only:
ofmust be omitted or"all". Every piece was verified by a real FreeCAD 1.1.0 recompute, including parameter-flip rebuilds (volume within 1e-3 relative). Seedocs/FCSTD_MAPPER.md.
FreeCAD 1.1 reader
- A lossless
.FCStdreader and editor (ops/fcstdReader.js). A no-op round trip is byte-identical; an edit rewrites only the changed element and marks the object Touched, so FreeCAD recomputes exactly what changed. It backscad_edit_guardedon.FCStdfiles. Seedocs/FCSTD_READER.md.
Verify before you save: cad_verify
- New tool
cad_verify. One read-only call after a build and beforecad_saveor export returns a ranked list of problems, each with a concrete fix (for example "set param hole_d = 8"): - validity: watertight, closed manifold, orientation, B-rep check, and a heal probe run on a rebuilt copy, never on the live body;
- topology: Betti numbers, holes and through holes;
spec: named dimensions with tolerances, measured on the body (a list, or text lines likehole_d = 8 mm ±0.05), each pass or fail with its delta;drawing: an engineering drawing PNG. The views are found, cropped, registered and named; first or third angle is detected; each view reports silhouette IoU, edge agreement, drawing-vs-model extents and drawn hole sizes;reference: a.stepor.FCStdmodel to compare against;export {format:"fcstd"}: mapper fallbacks (lost editability) and, with FreeCAD, a real recompute compared against NativeCAD's volume.- The model sheet and the per-view diffs come back as MCP image content, and are also written under
<output dir>/verify/<label>/.
Edit behind a firewall: cad_edit_guarded
- New tool
cad_edit_guarded. It applies param, feature or append edits to the session document or to an.FCStd, then proves nothing outside the edit changed. It derives the affected region from dependents and footprints (or takesexpect), recomputes, compares the surfaces outside that region, checks validity and anytargets, and rolls back on failure. An.FCStdis restored byte-identical, and the report says so.outwrites elsewhere;dryRunwrites nothing.
Other changes since 0.22.1
- Mesh to B-rep: the near-flat cone gate is on by default. A near-flat cone region that passes by its apex becomes a B-spline sheet instead of a pole cap. On the Rotor 67 blade, the body comes back closed (2.4 mm² of
#repairfill, was 557 mm²) and within -0.056 % of the mesh volume.nearFlatWrap:falserestores the old behaviour. - The
cad_dfm_checklisting summary is shorter (5164 to 5120 B), and the tools/list budget line moves from 385 KiB to 392 KiB (the 400 KiB ceiling is unchanged). The repro-synth and failure-report tools read their environment throughenv.js. cad_verifyshares the preview/silhouette family budget, andcad_edit_guardedshares the session-state family budget.- The FreeCAD paths work from the
.mcpb, plugin and npm bundle. The FreeCAD scripts (resolve_refs.py,step_to_brep.py,mapper_check.py,fcstd_to_step.py,edit_guard_check.py) now ship in the bundle and are written once per build under<cadsolverHome>/freecad/<sha12>/, because FreeCAD is another process and cannot read the bundle;CADSOLVER_FREECAD_SCRIPTSoverrides the directory. The body-scoped STEP filter moved into the kernel (ops/stepFilter.js; the old module re-exports it), so the bundle builds. - A missing FreeCAD is a clear error. The
.FCStdexport,cad_verifyandcad_edit_guardedpaths name FreeCAD 1.1 andFREECADCMDinstead of surfacingspawnSync freecadcmd ENOENT. AFREECADCMDthat points at nothing no longer raises an unhandled child-processerrorevent fromcad_edit_guarded.
NativeCAD 0.22.x
v0.22.1 “Dutoit”
Fasteners through existing holes, a fastener audit that recognises nuts and washers, a fast import replay, STEP trimmed curves read correctly, and self-update on by default for .mcpb and plugin installs. 180 tools, unchanged from 0.22.0. The command schema is unchanged.
Self-update
.mcpband plugin installs withautoUpdateunset now update themselves. WhenautoUpdatehas never been set, the effective mode isautofor the self-installing editions (.mcpb, plugin) andnotifyfor npm, dev and unknown installs. An explicit setting (off,notify,auto) still wins. Every gate is unchanged: signature and sha256, install only while the server is idle, an app reload finishes it, rollback after 3 failed starts,haltVersions, rollout. The update status reports where the mode came from (modeSource:settingordefault), and the reload notice says the install updates itself by default and how to change it. To keep the old behaviour:cad_configure {autoUpdate:"notify"}. Seedocs/UPDATES.md.
Fasteners
cad_fastenergoes through existing holes and slots. A part whose existing hole or slot the axis runs down now joins the stack instead of reading as a gap. A hole or slot already at least the bore the part needs, and open all the way through, is kept (the part is not drilled); a narrower or blind one is drilled to size. Each is reported infastener.existingHoles[](part,dia,shape,through,kept,enlarged,toDia,...). A threaded landing in an existing hole already as wide as its bore is refusedfastener-bad-parameter, namingnut: trueortapped: false.- The head-side washer sits on the first part's entry face, and the head on the washer; it used to be placed inside the part.
fastener.headUndersiderecords where the head bears. cad_fastener_auditrecognises nuts and washers by their geometry. A body with no declared role and no hardware name is measured: a bored hexagonal prism matching ISO 4032/4035 is a nut, an annulus matching DIN 125-A / ISO 7089 is a washer. A recognised nut is scored as a nut (no more "8 mm vs 10 required" tapped-landing reports) and must be coaxial with the shank. Newstats.washersandstats.detectedHardware;rules.detectHardware:falseturns it off.cad_fastener_auditaudits across documents. Bodies from several live documents are measured together in world space instead of refusingfastener-audit-cross-document; the newdocumentsfield names them and the response lists them.
Import
- Trivial ops beside a big STEP import are fast again. A document whose first op is a
cad_importcaches that import's result, keyed on the op, the resolved file's size, mtime and content hash, and the build options. A cold replay (the first write after an import, a write aftercad_delete_body, a journal restore, a branch, a second document importing the same file) restores it instead of translating the file again: about 12-21 s down to under 100 ms on a 23-body assembly.
STEP reading
TRIMMED_CURVEtrims are honoured: an edge on a trimmed curve is its basis curve between the edge's own vertices, so a cylinder's trimmed rims no longer import as straight chords (a filter canister imported flat before).- Two halves of one closed curve are not duplicates: a rim written as two complementary edges on one circle, ellipse or periodic B-spline is no longer reported as a duplicate pair (a FreeCAD fan reported 166 on a clean shell).
- Import health checks volume against the bounding box: a closed shell that is flat, empty or larger than its own box is flagged instead of passing as clean and watertight.
- Planar outlines project along the plane's normal, so a tall, thin wall standing at an angle is no longer collapsed into a segment that "crosses itself".
v0.22.0 “de Waart”
Coupled simulations, easing edges, the Space Program add-on, and sharper sketch and mesh answers. 180 tools: the 178 from 0.21.0 plus cad_couple and cad_ease_edges. The command schema moves from 1.62.0 to 1.63.0, and every change is additive. Installs on 0.17.2 and later pick this release up through the self-update once it is published.
Coupling simulations: cad_couple
- New tool
cad_couple(plan|run|replay|status). It couples a fast reduced-order model with higher-fidelity solvers used as calibrators: the model is re-solved with each measured correction factor as an explicit input, in an outer fixed-point loop (under-relaxation or Aitken) with a pass cap. Every run ends with a named status:converged,not-converged,oscillating,step-failed(a solver timeout included),replay-exhaustedorbudget-exceeded. A spec is JSON (coupling.spec.v1: factors, model, calibrators, gates, loop, overrides), passed inline or as a path. - Provenance and replay. Every factor comes back as a calibration record carrying the source run id, solver and version, host, ticket, commit, file sha256 and the source run's own timestamp.
runreturns a record (coupling.run.v1) with a content hash.replayre-runs it with no solver at all and reports whether it reproduced that hash. - Gates with cited targets. Each gate checks one physical quantity against a target fixed in advance, and names the ticket that set the target. A missing quantity fails its gate. A run is
okonly when it converged and every gate passed. - Solvers through the Solver Connectors. A live step runs SU2, OpenFOAM, Cantera or CalculiX through the connector contract, with a validated job spec and a wall-clock budget. Where the solver is not installed, the call is refused before anything is written:
E_PRECONDITION, reasonCodeSOLVER_MISSING, with a hint to run on the solver host or replay. Recorded calibrators replay results on any machine. - The turbojet's calibrations, ported. The Cantera burner fuel/air calibration reproduces today's cycle exactly. The SU2 nozzle Cd loop replays the two recorded SU2 runs, and its gate rows match what the original post-processing wrote. With the tighter design tolerance (|dCd| < 0.005) the replay reports that a third live pass is still needed. Method and limits:
docs/coupling/COUPLING.md. There is no per-timestep CFD coupling, no conjugate heat transfer and no blade-row CFD yet.
Modeling
- New tool
cad_ease_edges: ease every exposed edge to a maker's radius (fillet, with a chamfer fallback) and leave the faces that mate with other parts sharp. The radius comes fromradiusor arule("hardwood"2 mm,"plywood"1 mm,"aluminium"1.5 mm); mating faces come frommateWith("auto"finds every face touching another body). - Blade root blends are faster and lighter. A root blend that took 7.4 s now takes about 2 s, and its STEP drops from 2.03 MB to 248 kB (an HP turbine: 17.8 s to 2.6 s, 3.2 MB to 456 kB).
- Tessellation on spline faces: the deflection refine's start check measures the distance to the surface instead of a same-uv offset.
Sketches
- A conflict reply names every conflict it found. When a point is shared beside a collapsed line, or put on a line next to a conflict, the reply names both conflicts instead of the first one found, and a zero-length line found before the solve is named as a second conflict.
conflictSearch.minimalis one measured rule: it istrueexactly when deleting any one member ofconflictSetleaves a set the solver answers.
Meshing and analysis
- Volume meshing ignores stray vertices. Unreferenced input vertices are dropped before meshing (
stats.unreferencedVerticesreports them when there are any), and the boundary is now reported two ways:stats.boundaryVerticesKept(the vertex count) andstats.boundaryAreaCovered(the share of the boundary area covered by mesh faces), with...AfterImprovereadings on the final mesh.stats.boundaryPreservedis retired; readboundaryVerticesKeptinstead. - Cyclic modal results from CalculiX 2.17 are read correctly (one displacement block per degenerate mode pair), and a cyclic-disk solve can run in resumable chunks with a cache, so a long nodal-diameter sweep survives an interruption.
- Supports for metal printing (LPBF) follow down-facing skin, respect keep-out faces, and no longer bridge from one part to another by default.
- Host jobs are launched detached and tracked through their completion marker (
CADSOLVER_HOST_JOB_INLINE=1restores the old in-process path).
Space Program add-on
- Design a craft, then fly it.
cad_export_bodywithformat:"vehicle"writes anncsp.vehicle/1manifest and mesh payload from the session's bodies, andcad_configure {addon:{action, id:"space-program"}}installs, opens and removes the signed Space Program add-on. An exported craft gets its systems derived from the manifest. The plugin carries aspace-program-missionsskill that goes from a concept to a mission file.
Bug reports
- Improvements to the bug feedback loop and gathering. IP protections and checks enhanced.
NativeCAD 0.21.x
v0.21.0 “DePreist”
Solver Connectors, and a turbojet designed, simulated and costed end to end. 178 tools: the 177 from 0.20.0 plus cad_feature_edit. The command schema moves from 1.57.0 to 1.62.0, and every change is additive. Installs on 0.17.2 and later pick this release up through the self-update once it is published.
Solver Connectors: SU2, Cantera and OpenFOAM
- One connector contract for external solvers. A job spec and registry that every solver plugs into, live progress readers that report residuals and percent complete while a run is going, field export that maps solver results back onto the model, and
solverd, the background service that queues, runs and tracks solver jobs. - SU2, Cantera and OpenFOAM host tooling. One-command setup for SU2 (v8.5 release builds), Cantera and OpenFOAM, on plain Linux, Fedora or Docker, with convergence checks on every run and the results written back next to the design.
The turbojet pilot, end to end
- Tail cone and a re-sized exhaust nozzle, confirmed with an SU2 compressible-flow result.
- Cantera combustor network: stability margin, emissions (NOx, CO, unburnt hydrocarbons) and the blowout limit, on one chart.
- Rotor integrity: a turbine-disc packet with burst margin and stresses from a 1-D disc model.
- Off-design maps for compressor and turbine, a costed bill of materials, and a station diagram of the whole gas path.
- Start-up animation with balanced streamlines, held to a gas-path rule so flow only goes where the hardware routes it.
Modeling
- New tool
cad_feature_edit: change an existing feature's parameters in place ({"diameter": 6.6}) and rebuild everything that follows, instead of re-creating it. Returns the before/after diff. constrain_faceanddimension_facedocument operations: drive a face by a constraint or a dimension (schema 1.62.0).- Boolean fuzzy tolerance: booleans take a
fuzzytolerance so near-coincident faces from imported or hand-placed parts merge cleanly (schema 1.60.0). Cone, sphere and torus intersections are now computed in closed form inside the boolean. - Chamfers that hold up: a chamfer band can run onto a spherical face, and saddle corners no longer use up the operation budget before the chamfer is tried.
- Thread run-out: the run-out strip checks that the diameter fits its supports before building, and modelled bolt shanks can be partly threaded to the ISO 4014 / ISO 4762 thread length.
- PMI carries through copy, mirror and pattern, with a warning naming anything that could not be carried. Patterns are written as instances under AP203e2 / AP214 so hole callouts survive.
- Gears: straight bevel, spiral bevel and hypoid (zero offset) through
cad_gear.
Lattices, additive and export
- Lattice
cellAspectandbuildDir: stretch the cell per axis and set the build direction (schema 1.58.0 / 1.59.0). The lattice report givesselfSupport, the minimum strut elevation against that direction, so a lattice that prints without supports can be designed rather than guessed. - Conformal lattices run as one continuous frame across multi-face skins, and TPMS wall thickness is calibrated.
cad_export_bodyformat"ply"and print profiles withresolution(schema 1.61.0). Field bodies too large for memory now stream to 3MF and to the print profiles instead of failing.
Viewing and checking
- Section caps without z-fighting: cut faces in section views render cleanly, and a coincident-face check flags overlapping faces before they cause trouble in a render, a boolean or an export.
Long calls, fonts, sandbox mode
- Long calls detach into async jobs by estimated cost.
cad_fea_runwhose server-side estimate (target cells x bodies x refinement solves) is over ~40 s, andcad_importof an STL/OBJ of >= 100k triangles, now answer with{jobId, stage, etaMs, auto}instead of the inline result; fetch it withcad_job_result {jobId, waitMs}(same response as the inline call). In-processtools.callcallers stay synchronous.CADSOLVER_MCP_JOB_COST_Smoves the line (offdisables). Jobs submitted with a pre-flight estimate (mesh_to_brep) now report a non-nullpct(elapsed / estimate, capped at 95) andetaMswhile running, flaggedestimate: {ms, basis:"preflight"}; measured component progress still wins where the build reports it. - Bundled fonts now ship in the .mcpb.
cad_engrave_text {font:"dejavu-sans" | "dejavu-sans-bold" | "liberation-sans"}failed from an installed extension withENOENT ...kernel/fonts/<file>.ttfbecause the staged kernel never carriedfonts/. The .ttf files and their licence texts are now staged assets (src/font-assets.js). A bundled font id whose file is missing is now refused on fieldfontwith "bundled font ... is missing from this install", naming the fonts that can be used, instead of a raw ENOENT.cad_server_info {section:"services"}gainsservices.fonts={ bundled, missing, unmanifested }. cad_picogkfixes from the first real picopie run.volumeMm3of a hollow result (shell, TPMS sheet) is now its solid volume; it used to be the enclosed volume, about 3.7x too high on a 1 mm shell.kindis"brep"only whento_brepactually built a B-rep; when it did not (TPMS and lattice meshes trip mesh_to_brep's inline budget), the result iskind: "mesh"with ato_brepwarning and anext. The result STL is imported welded, so it is watertight.importedis populated (it was always null). A result of 100k or more triangles no longer comes back withhandle: nullfrom ajobMode: falsecall.- Sandbox mode (
NATIVECAD_SANDBOX=1): the server keeps file access inside its workspace and refuses outbound network at runtime, for regulated and air-gapped deployments.
NativeCAD 0.20.x
v0.20.0 “de la Parra”
Metal 3D printing: DMP Flex 350 build prep and quoting in one call. 177 tools: the 172 from 0.19.0, plus cad_am_prep, cad_picogk, cad_combustion, and two document ops that are now listed as tools, cad_face_region and cad_mutual_trim. The metal-AM chain (NLAM [2]–[9]) and the PicoGK interop (NLAM [20]–[22]) land together, alongside STEP-version save and compressed STEP. Installs on 0.17.2 and later pick this release up through the self-update once it is published.
Kernel SDKs now need a licence key: @nativecad/kernel 0.3.2 and @nativecad/kernel-sdk 0.2.2 refuse with E_LICENSE_REQUIRED until a key is active — a license key is required. Commercial: https://featureboard.ai/nativecad/web/pricing.html. Non-commercial (education, research, makers): keys are issued case by case — request one at support@featureboard.ai. The self-serve free key from featureboard.ai/register covers the MCP plugin only: the kernel and kernel-sdk refuse it with E_LICENSE_REQUIRED, reason free_tier_not_kernel (a key issued case by case carries the kernel entitlement). The kernel ships as nativecad-kernel-0.3.2.tgz. The MCP server's own licensing is unchanged.
Metal LPBF on the 3D Systems DMP Flex 350 (NLAM [2]–[8])
- LPBF DFM pack
lpbf_dmp_flex350:cad_dfm_check {process:"lpbf_dmp_flex350"}checks minimum wall (0.8 mm), part envelope (275 x 275 x 420 mm), self-supporting overhang (45 deg) and trapped powder (lpbf_trapped_powder, blocking for an enclosed cavity with no drain hole). Rules live indata/dfm/lpbf_dmp_flex350.json. - Plate nesting:
cad_nesttargetdmp_flex_350nests on the DMP Flex 350 plate at the 5 mm LPBF default spacing, opens a second plate when one is full, and exports format"3mf"(one object per part). Asking an LPBF nest for"3mf-bambu"is refusedE_INVALID_ARGS. - Build orientation:
cad_build_orientationpacklpbf_dmp_flex350ranks the six axes, the planar faces and a hint by support volume, the LPBF metrics and a recoater-risk proxy, and explains every term. - Time and cost quote: an LPBF build-time and cost model (
ops/lpbfBuildCost.js, shop profiledmp_flex_350_default). Scan time scales with laser count, recoat time does not. Profile fields left null are listed inquote.assumptions, andsensitivitynames at least three cost drivers. Calibration is indocs/additive/LPBF_COST_MODEL.md. - Metal supports:
cad_supportprofilelpbfmakes block, line and cone supports with teeth and fragmentation, only under real overhangs past the pack's self-support limit. - Plate export and CLI: a 3DXpert-ready plate folder with one 3MF (and an STL fallback) per object, covering each part copy and each support region, plus
plate.jsonwith the transforms and open CLI layer slices. The machine's job file is still written in 3DXpert, since DMP Control accepts no public job format.
Metal build in one call: cad_am_prep (NLAM [9])
- New tool
cad_am_prep(groupprint, budget familyreport): LPBF DFM (packlpbf_dmp_flex350) -> orientation -> machining-allowance print body -> LPBF supports -> DMP Flex 350 plate nest -> time/cost quote -> files (plate folder with 3MF/STL per object,plate.json, CLI; STEP print body, AP214). The schema is published top-level only, andcad_helphas every field. - New refusal vocabulary in
details.code:AM_PREP_DFM_BLOCKED(E_GEOMETRY;details.blocking,details.findings,details.remedies),AM_PREP_NEST_UNVERIFIED/AM_PREP_NEST/AM_PREP_ORIENTATION(E_GEOMETRY).force: truecarries on past a DFM block and records it. - Sales proof pack for L41:
samples/leads/L41-next-level-am/am-prep/has an NGV doublet (qty 1) and a bracket (qty 20) run end to end.
PicoGK interop and cad_picogk (NLAM [20]–[22])
- PicoGK file io:
cad_export_bodytakes format"picogk_stl"(a binary STL that is watertight, has outward normals and uses chord tolerance voxel/4, withvoxelSizein mm) and"vdb"(OpenVDB, through the optional picopie add-on).cad_importreads a PicoGK STL or.vdbback withvoxelSizeandtoBrep, which rebuilds a B-rep and reports fidelity.voxelSizeon any other format is refusedE_INVALID_ARGS. When picopie is missing, the refusal is namedPICOGK_ADDON_MISSING. - New tool
cad_picogk: runs PicoGK voxel operations (voxelize, offset, shell, smoothen, boolean, TPMS implicit, beam lattice) on NativeCAD bodies as an async job, and brings the result back as a mesh body, or a B-rep withto_brep. The default voxel size is 0.25 mm. PicoGK runs outside this process. With no host, the call is refusedE_UNSUPPORTED/PICOGK_RUNTIME_MISSINGwith the install remedy before a job is queued. Host-side setup is indocs/interop/PICOGK.md. cad_picogkmodecem:cem: {project_path, entry, params?, output_dir?, maxOutputMB?}builds a customer's own PicoGK / ShapeKernel C# project with the.NET SDK, runs it headless, and imports every STL / VDB it writes underoutput_dir. Paths are sandboxed toCADSOLVER_CEM_ROOT(by default the output directory), and absolute,.., symlink andoutput_direscapes are refused by name. When there is no.NET SDK, the call is refusedE_UNSUPPORTED/CEM_DOTNET_MISSINGwith the install remedy.PICOGK_IMPORT_FAILEDis returned when an output cannot be imported.- Explicit + PicoGK showcase:
samples/leads/L41-next-level-am/explicit-picogk/contains a cooled liner (one exact solid) and a lattice route, with the stand-in labelled when PicoGK was not used.
Combustor chemistry: cad_combustion (CCOMB [2/9])
- New tool
cad_combustion: Cantera chemistry for early combustor design — equilibrium (adiabatic flame temperature), laminar flame speed, ignition delay, and PSR/PFR reactor networks with staged air, NOx/CO/UHC, combustion efficiency and a PSR blowout search. The listing is top-level only (mode,fuel,phi,T,p, the rest underspec);cad_help({tool:"cad_combustion"})has the full schema. - Cantera is an optional add-on and is never bundled. Without it the call is refused before any job is queued,
E_PRECONDITIONwith reasonCodeCANTERA_MISSING(orCANTERA_PYTHON_MISSING/CANTERA_WORKER_MISSING) and the pip/conda remedy indetails.install. Bad requests are refusedE_INVALID_ARGS/CANTERA_BAD_REQUEST.
Turbojet sample, cycle, meanline and SU2 (kernel only, not MCP tools)
- Turbojet sample:
samples/leads/L41-next-level-am/turbojet/has the impeller, diffuser, NGV, turbine and liner built from the meanline, with each part watertight, re-read from STEP AP242 within 0.1 % volume, with LPBF DFM and orientation recorded and nested on the DMP Flex 350. The assembly is inassembly.step. - The 1D turbojet cycle, the meanline-to-geometry step (
packages/cfd/src/cycle/meanline.js) and the SU2 compressible RANS provider (packages/cfd/src/providers/su2*) are in the kernel and are used by the sample. They are not exposed as MCP tools in this release.
Two document ops now listed as tools
cad_mutual_trim(command schema 1.52.0) andcad_face_region(command schema 1.54.0) are derived from the command schema like every other document op, so they appear in tools/list. The command schema is now 1.57.0 (lattice_fillkeepSolid / frame / field output and thefieldop). Every change is additive, so documents valid at 1.54.0 still validate.
STEP versions: save as AP203, AP214 or AP242
cad_saveandcad_export_body(format:"step") takeschema:"ap203e1","ap203e2"("ap203"),"ap214","ap242e1","ap242e2"("ap242", the default),"ap242e3", or"source"(keep the schema an imported file declared). "Save this as AP214" writesFILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'))with the matching application context.- The result carries
schema{id, fileSchema}(oncad_saveasreport.saved.schema), plusdropped/expanded/warningswhen the schema could not carry part of the document — semantic PMI andtessellatedare AP242-only; AP203 ed1 has no colours, layers or validation properties and is written flat. strict: truerefuses such a write instead:E_STEP_SCHEMA_UNSUPPORTED, nothing written. Oncad_export_bodyit is the samestrictthat already makes the assembly check refuse.schemaon any othercad_export_bodyformat is refusedE_INVALID_ARGS/STEP_SCHEMA_FORMAT;ap242-xmlstays AP242. A call that names no schema writes exactly the bytes it did before and reports noschemakey.
Compressed STEP:.stpZ
- A
cad_save/cad_export_body(format:"step") path ending in.stpZ/.stpz, orcompress: true, writes the file gzipped — the compressed STEP ("STEP-Zip") CATIA, SolidWorks, NX and most PDM systems open.bytesis then the file's size anduncompressedBytesthe Part 21 text's (a NIST CTC-01 part: 235 KB -> 40 KB, 5.8x). Output is byte-reproducible (gzip MTIME 0, OS byte 255). cad_importreads.stpZback, and any STEP whose bytes start with the gzip magic, whatever its name; a truncated one is refusedSTEP_GZIP_CORRUPT.compresson any other format is refusedE_INVALID_ARGS/STEP_COMPRESS_FORMAT, nothing written.
NativeCAD 0.19.x
v0.19.0 “Christie”
172 tools: the 170 from 0.18.3, plus cad_rtl and cad_synth. Kernel and tool fixes from master since 0.18.3: a new boolean rung that trims a solid of revolution by one analytic surface, an FEA path with no dead end when no solver is installed, cad_pcb action:"package", and eight reported-issue fixes. Installs on 0.17.2 and later pick this release up through the self-update.
Modelling kernel
- A round boss on a cone or cylinder can be trimmed at the wall. A revolved body cut by one plane, cylinder or cone (subtract, intersect, or
cad_split_planewith a cylinder or cone cutter) now goes through an exact trim: the cut rim is a closed B-spline within 1e-6 of the part size of both surfaces, with that tolerance recorded on the edge, and one analytic cap face. These cuts used to be refused (unsupported_overlap, the genBool validity gate, or "crosses 3 faces") or came back as hundreds of faceted faces; they now answer in about a second with 9 faces. Refusals are namedrvtrim_*and leave nothing behind.CADSOLVER_NO_REVOLVE_TRIM=1turns it off. - Cylinders unioned onto a prism's flat cap stay analytic. Four r16 pads onto a drone cross fold in about 0.3 s (was 1.6–1.9 s, and 39 s on 0.17.x), with an exact closed-form volume.
CADSOLVER_NO_ARC_PRISM_UNION=1turns it off. cad_shellon a part with a narrow neck refuses by name (shell-cavity-pinched, with the widest wall that fits) instead of failing in the general engine.- Sliver and spike fixes in surface continuity analysis (
cad_surface_analysis): a face inversion next to a C0 kink no longer reads a leaning normal, so phantom G1 breaks are gone. - Sketch solves that converge through zero radius are read by their mirror instead of answering a negative arc; a line collapsed to nothing returns a warning.
- Drawings: a part with two equal principal moments (a symmetric frame) is oriented by its model or symmetry axes, not by eigen-solver noise.
- Import health reports faces whose tessellation does not cover the face (
tessellation_coverage_mismatch). - The kernel bundle is about 150 KB smaller gzipped: the DFM family, tolerance stack and sketch load lazily again.
FEA without a solver
cad_fea_caseno longer needs a solver installed, and exports the case as an Abaqus/CalculiX.inpor Nastran.bdfdeck from one mesh, each with how to run it.cad_fea_runtakesdeckOnly. When no solver is found, the refusalsolver_missingnow carries the deck and a setup block (download, package-manager line, retry) instead of a dead end. A solve is refused only when a solver is declared absent.cad_modalbehaves the same way.
PCB and chips
- New tool
cad_rtl:actionlint(verilator),sim(iverilog + vvp over a JSON test-vector table or a cocotb module: per-test pass/fail, the first failing cycle with the signals around it, a waveform excerpt, an optional VCD) andcov(toggle coverage). A missing simulator is refusedE_PRECONDITIONwithreasonCode:"tool_not_found"and how to install it; long simulations run as a job. - New tool
cad_synth: yosys synthesis against a PDK liberty, with a lint gate, area and cell statistics, latch / combinational-loop / unmapped-cell findings and a reproducible script. A missing yosys or liberty is refused by code. cad_pcbtakesaction:"package"for package-level checks (FCBGA, chiplet, HBM, wire-bond) and a die/package/board co-design check.- Ampacity is compared against openly published data, and ODB++ reading gains the KiCad 9 dialect,
.Z/.zipcontainers and pinned stackup attribute names.
Fixed reported issues
- A round boss at an angle on a cone could not be trimmed at the wall.
- A multi-tool union refused
non_connected_resultwhen one tool only joined after the others; it is retried and the union refuses only when the set is really disconnected. - An extrude with a hole loop crossing or leaving the outer profile, or with crossing holes, is refused by name (
E_PROFILE_HOLE_CROSSES_OUTER,E_PROFILE_HOLES_CROSS). - A closed B-spline rim was flagged as a zero-length degenerate edge, so watertight spline lofts failed the check.
- A union through a planar face with holes did not weld on every side.
cad_tube_sweepreports its volume from the analytic result without tessellating.- A lug on a revolve's seam plane fanned flat triangles across the cylinder.
build_scene --partaudits only the named parts.
NativeCAD 0.18.x
v0.18.3 “Chan”
170 tools, the same as 0.18.2. New command: nativecad-impactdiff, the PCB revision impact diff for servers. No tool changes. Installs on 0.17.2 and later pick this release up through the self-update.
- A second bin in the package:
npx -p nativecad-mcp nativecad-impactdiff --before a.kicad_pcb --after b.kicad_pcbprints impactDiff v1 (the same JSONcad_pcb {action:"diff"}returns).--repo/--before-ref/--after-ref/--pathreads two commits without a checkout. --batch jobs.jsonlruns many pairs through one engine (boot once), one result line per job: the shape of "every commit of this repo".--targets targets.jsonpasses the project's target values (impedance classes, rails, cert, stackup, ...) to the diff, so impacts are judged against limits.- Offline by default: telemetry and auto-update are off for this command; with
CADSOLVER_FLEET_TOKENthe engine runs in worker mode (licence never written to disk). - Exit codes for CI and workers: 0 ok, 1 an impact at or above
--fail-on, 2 usage, 3 licence required, 4 refusal or engine error. Output is deterministic (no wall time, no temp paths). Guide: docs/pilots/bomblame-embedding.md. - Needs nothing from node_modules (a built-in stdio client talks to the bundled engine), so an air-gapped server can unpack the
.tgzand runnode package/bin/nativecad-impactdiff.jsin place.
v0.18.2 “Canellakis”
170 tools, the same as 0.18.1. US spellings for the colour options. Installs on 0.17.2 and later pick this release up through the self-update.
cad_set_parttakescoloras well ascolour;cad_saveandcad_export_bodytakecolorsas well ascolours. The two spellings are the same option and write the same file. Giving both with different values is refusedCOLOUR_ALIAS_CONFLICT; the same value twice is accepted.
v0.18.1 “Bülow”
170 tools: the 168 from 0.18.0, plus cad_cert (certification evidence plan and packet) and cad_feature_pattern (pattern a pad, pocket or hole inside one body). Part colours in STEP exports. Installs on 0.17.2 and later pick this release up through the self-update (cad_configure {update:"now"}, or by themselves under autoUpdate:"auto").
Colours in STEP exports
cad_set_parttakes acolour("#RRGGBB","#RGB", or[r,g,b]with 0..1 or 0..255 components). The colour is written into every STEP export (cad_save,cad_export_bodyformatstepandap242-xml) as one body-levelSTYLED_ITEM/COLOUR_RGB, the AP214/AP242 form Onshape, SolidWorks, Fusion and OCCT read, and intox_tasSDL/TYSA_COLOUR.cad_set_partechoes it back as hex.cad_saveandcad_export_bodytakecolours, a map from a body handle, op id or part name to a colour, for a one-off colouring at export time. A key that names no body is refusedCOLOUR_NO_BODY, with the document's bodies listed; nothing is written.- Colours persist by default. A coloured STEP that is imported and saved comes back out coloured. A coloured part keeps its colour through later edits (fillet, chamfer, boolean, transform, pattern): a face the edit created takes the colour of the face it came from, or the part's colour. Part names and colours set with
cad_set_partalso survive later edits of the body; before this release a stamp was dropped by the next modelling call. - CAx-IF model styling. Body colours with face overrides and layer assignments (including hidden layers) follow the CAx-IF Recommended Practice, checked by
tools/caxif/styling_check.js. The written colours, layers and assembly tree were read back by OCCT (cadquery-ocp 7.8.1) and match. - Known gap:
cad_savestill names bodies by op id;cad_export_bodywrites thecad_set_partnames.
v0.18.0 “Brico”
168 tools: the 166 from 0.17.2, plus cad_configurator and cad_interfaces. Nothing was removed or renamed. Installs on 0.17.2 pick this release up through the self-update (cad_configure {update:"now"}, or by themselves under autoUpdate:"auto").
New tools
cad_configuratorworks with product families: a JSON file that lists the modules a product can be built from, which ones are required, how they may be combined, and where they connect. Its actions are:validate: checks a family file and a chosen configuration, and names every rule it breaks.enumerate: lists the valid configurations.nearestFix: for an invalid configuration, the smallest change that makes it valid.build: places each module at its interface frames (with offset, rotate and flip), builds the parts, names them, and returns the bill of materials. It can also export the product as one named STEP file with an interference report.
The family can be given inline or as a path. Schema v1.2 adds interface mate frames, module geometry given as a tool call, a STEP path or a batch, part quantities, and a root. v1 and v1.1 files still load. Placement errors are refused by name.
cad_interfacesfinds how a part connects to other parts: hole patterns (rectangular, circular, linear, with pitch, diameter and thread, from PMI or an ISO 273 guess), pin headers, threaded bores (CS-mount, C-mount, tripod), named port bodies, and mounting faces. Each one comes back with a coordinate frame, in the form Onshape uses and in the form a product-family interface entry uses. On the official Raspberry Pi 5 STEP it finds the 58 × 49 mm M2.5 pattern and the 40-pin header frame exactly.
Drawing to STEP, in the plugin
- The Cowork
.pluginnow ships adrawing-to-stepskill. It walks Claude through turning a PDF or DXF drawing into a STEP model, a Parasolidx_tand a dimension table, and through checking the result against the drawing. It includes a batch read-out script for a folder of drawings (inventory and a summary CSV) and a fallback for scanned (raster) PDFs. - Tested on Raspberry Pi's own drawings for the HQ Camera, Camera Module 3 and Active Cooler.
cad_importdrawing mode reads a sheet whose line work is all on one annotation-named layer (e.g. "Dims") by stroke style instead of refusing it (PDF_NO_VIEWS).
Cleaner exports
- The STEP header no longer carries an author or organisation by default. Both are empty unless you set them (an export option or an environment variable), and
originating_systemnames NativeCAD and its version. - STEP assemblies carry AP242 assembly validation properties by default, next to the per-part volume, area and centroid they already had. The CAx-IF validation-property checker grades the Raspberry Pi GS camera assembly 28 geometric and 2 assembly rows OK, 0 bad. A receiving system can use them to confirm it read the parts and the assembly correctly.
- The Parasolid
x_twriter no longer splits a token across two lines. Long strings used to be wrapped at 80 columns, which some readers rejected. - Part names, colours and the assembly structure survive a STEP round trip, which is what an Onshape import keeps (Onshape reads AP242 geometry and face colours, not PMI).
NativeCAD 0.17.x
v0.17.2 “Berglund”
166 tools, the same as 0.17.1. The self-update fixed for real installs. Installs on 0.17.0 and 0.17.1 cannot install this release by themselves, because of the two bugs it fixes, so install it by hand once. From 0.17.2 on, updates install themselves.
- A check answered from the cache no longer locks every later check. In 0.17.0 a cache hit finished synchronously, so the code meant to clear the in-progress marker ran before the marker was set. The finished check then stayed in place, and every later check,
update:"check"andupdate:"now"included, kept answering whatever the cache said. On the first install seen this way, that was "0.16.0 is the latest" for a day, while a signed 0.17.1 was live. - A Claude Desktop install is recognised as one even when it runs on the system Node. The install type now comes from the files around the running bundle (
manifest.json→ mcpb,.claude-plugin/plugin.json→ plugin,node_modules→ npm,src/server.js→ dev), not fromprocess.execPath. A.mcpbthat Claude Desktop launched with the systemnodeused to reportedition: "dev"and only ever notify. - A cached manifest older than the running version is ignored and fetched again, rather than trusted for 24 hours.
- The build smokes (
bundle-smoke,mcp-smoke) run withNATIVECAD_AUTO_UPDATE=off, so a build can no longer write a stale cache into the builder's own~/.nativecad.
v0.17.1 “Bátiz”
166 tools — the same 166 as 0.17.0. The only change is the listing copy. This is the first release delivered by the self-update that shipped in 0.17.0: an install on 0.17.0 sees it on _meta.update and in cad_server_info.update. Run cad_configure {update:"now"} to install it, or set autoUpdate:"auto" and it installs itself.
- The listing says what NativeCAD does. The Claude Desktop extension page (
.mcpbdescriptionandlong_description), the Cowork plugin description, and the npmpackage.jsondescription now describe the product: modelling, the import formats, DFM, FEA, drawings, PCB analysis, printing, and the self-update. Keywords were added for dfm, fea, drawings, pcb, 3d-printing and sheet-metal.
v0.17.0 “Asahina”
166 tools: 159 from 0.16.0, plus 7 new ones. Nothing was removed or renamed. The new tools are cad_drawing, cad_enclosure, cad_gdt_position, cad_pcb, cad_sheet_convert, cad_sketch_solve and cad_surface_analysis. The headline is that NativeCAD now updates itself. This is the first release that can, so 0.16.0 and older have to install 0.17.0 by hand once.
New tools since 0.16.0
cad_pcbanalyses a PCB layout without building solids. It reads.kicad_pcbfiles, Gerber X2 folders and IPC-2581.xml. Its actions are:analyze: impedance, DC drop, ampacity, thermal, UL 796 / IPC-6012 / creepage checks, and return path.bom: the parts list.diff: compares two revisions, given as files or git refs, and answers impactDiff v1 (below).
Every result carries an assumptions ledger, and a missing input is refused by name. cad_pcb needs a licence key.
cad_drawingturns a part or an assembly into a shop-ready drawing set: PDF, plus SVG and DXF per sheet (below).cad_enclosurebuilds a base-and-lid enclosure around a board envelope, with port cut-outs, mounts suited to the manufacturing process, a snap, screw or tray lid, and vents.cad_gdt_positionsizes and checks the position tolerance of a hole pattern from its fasteners (ASME Y14.5 floating and fixed fasteners, with an Ⓜ/Ⓛ bonus-zone wall check).cad_sheet_convertturns a solid mould into sheet metal: flat blank, rips, unfold and cuts.cad_sketch_solvesolves a constrained 2D sketch. It returns exact coordinates and the remaining degrees of freedom; when the sketch cannot be solved, it names the smallest set of constraints to delete.cad_surface_analysisis a read-only class-A surface check: continuity (G0/G1/G2 per shared edge), curvature, zebra stripes, deviation from a reference, and draft.
NativeCAD updates itself
- Release check. The first tool call of a session starts one background GET of
https://featureboard.ai/nativecad/releases/latest.json, cached for 24 hours. It sends no body and no identifiers, and it never blocks or fails a call. When a newer release exists, every response carries_meta.update({current, latest, verified, action, message}), andcad_server_infohas a newupdatesection (mode, channel, edition, install route, latest, verified, rollout, any rollback). cad_configuresettings.autoUpdate:"notify"(the default) reports new releases,"auto"installs them,"off"makes no network call at all.updateChannel:"stable"(the default) or"beta". Both are persisted like the other settings.update: "check"forces a fresh check, andupdate: "now"installs the latest verified release in any mode exceptoff. Underoff,updateis refused withE_PRECONDITION.- Only signed releases install.
latest.jsonis now signed with ed25519 (signature,keyId, andfiles[{key, edition, file, url, sha256, size}];tools/sign_release.js). It is verified against the public key built into the server (key idd2791510af4077e7), and the download must match the signed sha256 and size. A release that is unsigned, or signed with any other key, is reported but never installed, and the notice says why. The key can be rotated throughnextPublicKey, inside the signed body. - Where installs happen. Updates install into Claude Desktop / Cowork
.mcpbinstalls and Cowork.plugininstalls: the verified archive is written over the directory the running bundle was loaded from. An npm install is told thenpmcommand, and a source checkout is told to use git. Nothing is written while a tool call is in flight. The running process stays on its own version, because the kernel worker is pinned to a snapshot of the old bundle. Restarting Claude finishes the update. - Rollback. The previous version is kept under
~/.nativecad/install-backups/. A version this updater installed is on probation until one of its boots proves healthy (the first successful tool call, or 60 s up). The start that finds 3 boots of it died first restores the previous version, skips the bad one from then on, and says so on the next response. A version the publisher lists inhaltVersionsis rolled back the same way.channel,rolloutPctandhaltVersionsin the manifest stage a release. - State lives in
~/.nativecad/update/(NATIVECAD_HOMEmoves it). The other environment variables areNATIVECAD_AUTO_UPDATE,NATIVECAD_UPDATE_CHANNEL,NATIVECAD_UPDATE_URL(https, or a 127.0.0.1 address for tests) andNATIVECAD_UPDATE_DIR. Seedocs/UPDATES.md. - 0.16.0 and older have no updater, so 0.17.0 has to be installed by hand once. Releases after it install themselves.
cad_pcb: action: "diff" and action: "bom"
cad_pcb {action:"diff", before, after}compares two revisions of a board and answers impactDiff v1 (schema: "nativecad.impactDiff/1", docs/PCB_MCP.md section 6.3): the layout diff, the BOM diff, then the analyses re-run on both revisions for the changed nets and parts only, asimpacts[]withbefore/after/delta/limit/severity/why/causes, worst first, plussummary {counts, headline, markdown}. The same inputs give byte-identical JSON; a pair with no change givesimpacts: [].- Revisions from git.
board,beforeandaftertake{repo, ref, path}: a local repository read with git plumbing (rev-parse,cat-file,show,ls-tree;execFile, no shell), never checked out. The.kicad_proand.kicad_schnext to the board come from the same commit. New refusals:pcb_git_unavailable(E_UNSUPPORTED),pcb_git_ref_not_found(E_NOT_FOUND), andpcb_diff_incomparable(E_PRECONDITION, one side has nets and the other does not). Arefstarting with-, or apaththat is absolute or contains.., ispcb_bad_input. cad_pcb {action:"bom", board, schematic?}answers the parts list in BOMblame's split:lines[]withcategorySCH/PCB/BOARD_FEATURE,groups(totals per category) andexcluded.- Changed meaning:
diffandbomno longer refuse withE_UNSUPPORTED/pcb_option_not_built. Without their boards they refuseE_INVALID_ARGS/pcb_bad_input, naming the missing argument. - A diff whose changed set needs
dcorthermalon both revisions runs as an async job unlessjobMode: false. cad_pcb's tools/list entry grows 392 B, to 2984 B, inside its 4 KiB line.
cad_drawing: make the drawings
- New tool,
cad_drawing. One call turns a part or an assembly into a shop-ready drawing set. The input is a session body (of) or a customer STEP file (file). It plans the views, sheet size and scale; adds a shop profile's title block (fromdocMeta), general tolerance block and revision table; applies the default GD&T scheme and its table (gdt: "auto",gdtStyle,process,fasteners); adds dimensions with hole notes; and, for an assembly STEP, adds a parts list from its product structure. It writes a PDF (one page per sheet) plus an SVG and a DXF per sheet. The reply is{sheets:[{n, size, scale, views, files}], lint:{errors, warnings}, gdt:{datums, frames, rationale, tablePlacement}, warnings}: paths, never bytes, under 32 KB. - The tolerance lint is a gate. A sheet with a dimension that resolves to no tolerance source, or to more than one, is refused with
E_PRECONDITION,details.reasonCode: "drawing-lint-errors", anddetails.offendingnaming the dimensions. No file is written.allowLintErrors: truewrites the sheet and says so inwarnings. The other refusals aredrawing-file-not-found,drawing-file-not-step,drawing-bad-scale,drawing-no-views,drawing-no-solids,drawing-failed(E_GEOMETRY) anddrawing-write-failed(E_IO). - It runs on the kernel worker when the worker holds the document, through the read seam (
READ_OPS.DRAWING), and emits one progress event per sheet. It draws from a scratch copy, so the session document is not changed: section cuts never add bodies to it. cad_help({topic: "drawings"})is the playbook.cad_design_knowledge'sgdtpack points at the tool, anddocs/TOOLS.mdhas a worked example.
NativeCAD 0.16.x
v0.16.0 “Talich”
159 tools — the same 159 as 0.15.0: nothing added, removed or renamed. Command schema 1.41.0 → 1.42.0, additive: import gains page, and mode: "drawing" accepts a .pdf. The headline is PDF: cad_import reads one page of a vector PDF drawing with mode: "drawing" and answers what the same sheet as a DXF answers. DXF import now also reads profiles on tilted planes, and a big nest or drawing fits in the reply. docs/DXF_IMPORT.md is the reference.
cad_import reads PDF drawings
cad_import { path: "sheet.pdf", mode: "drawing", page? }reads one page of a vector PDF (the usual manufacturer or datasheet drawing: outlines as path operators, dimensions as positioned text). The reply is the samedrawinga DXF sheet gives: views, projection (first or third angle), each view's scale measured from its own dimensions, dimensions, title block, border and warnings. Nothing is built, and the reply carries adrawingId.saveCurvesanddrawingView(below) work as they do for a DXF.- Options.
pageis 1-based, default 1.unitsis"mm"or"in"and names the unit the dimension text is written in; the sheet itself is always paper millimetres.layers/excludeLayersfilter on the PDF's own layer names, or on the reader's stroke-style names when it has none. - A
.pdfis only ever a drawing. Withoutmode: "drawing"it is refused by name at validation, with the remedy (add the mode; for a sketch or a flat part, import the drawing's DXF).thickness,gapTol,spaceandlayouton a.pdfare refused by name too, and so areunitsother than mm / in andpageon any other path. All of these areE_INVALID_ARGS. - The report.
entry.imported.pdfcarries the page (index, count, size in mm, rotation), the PDF version, the layers and the role each one got, what the reader skipped (filled paths, images, clipping paths, invisible OCR text), the reader's own whole-sheet scale vote and the dimension text's unit.entry.drawing.pagesays which page was read. - A scan is refused, never guessed at. A page of images with no vector line work is
PDF_RASTER_ONLY:detail.ocrTextcarries any invisible OCR text, and the remedy asks for the vector PDF, a DXF or a STEP. The other refusals, each ondetails.reasonCodewith a remedy, arePDF_NO_VECTOR,PDF_NO_VIEWS,PDF_EMPTY_PAGE,PDF_ENCRYPTED,PDF_NOT_PDF,PDF_NO_PAGES,PDF_MALFORMEDandPDF_UNREADABLE.PDF_PAGE_RANGE(a page past the end; the hint gives the page count) comes back asE_INVALID_ARGS, on the import and ondrawingView. - Limits. A 3D PDF's embedded PRC / U3D model is not read. Text drawn as outlines is skipped, so a title block set in outlined glyphs has no fields. On a sheet dimensioned in inches, a view's measured scale reads the values as millimetres; a warning says so, and a title-block SCALE stands.
cad_importstill needs the licence key, for a PDF as for every import since an earlier release: an unlicensed server refuses it withE_LICENSE_REQUIREDbefore reading the file.
DXF import
- Profiles on tilted planes. Circles and arcs whose extrusion direction is off +Z, 3D LINEs and 3D POLYLINE / SPLINE points are grouped by the plane they lie in and chained on that plane, arcs kept exact. With
thickness, a profile on such a plane is extruded along the plane's normal from the plane. Without it, the sketch is drawn on one plane (world XY when that has profiles, otherwise the tilted plane), and profiles on any other plane are warned about and left out. Each plane is reported onimported.dxf.planes3dwith its frame, counts andnormalSource. An entity that states an extrusion direction decides the normal ("extrusion"); otherwise it points +Z, else +Y, else +X ("canonical"), so an outline extrudes the same way whichever direction it was drawn in. A 3D SPLINE's end tangents are projected into the fitted plane. - Closed fit-point-only SPLINEs are interpolated periodically, so the seam is smooth (C2) instead of kinked. Open splines are unchanged.
- Multiline attributes. An MTEXT-backed ATTRIB or ATTDEF is read whole, in either layout the DXF reference allows, and flagged
multiline: true, so a long title or revision field carries its full value. Inmode: "drawing"a multiline attribute is framed like an MTEXT, from its attachment point (group 71, reported asattachment), instead of as a one-line baseline. - Many bodies fit in the reply. Past 24 bodies (a DXF nest with
thickness, or a many-solid STEP), eachbodies[]row is compact{handle, name, bbox, volume, watertight}, the first 100 are listed, andbodyRowssays so and where the rest are.imported.dxf.bodies[]rows become{bodyRef, name, holes}and profiles{id, holes, area}. A 44-outline nest now answers in about 47 KB (it used to spill to a file) and 400 outlines in about 67 KB. The budget hint on a spilledcad_importreply names the DXF remedies: import in pieces by layer, or as a sketch. drawingViewis a new argument that the MCP layer serves itself. Withmode: "drawing",drawingView: {view, kinds?, offset?, limit?}returns one view's curves inline, about 40 KB a page, withnext.offsetfor the next page, for a client that cannot open thesaveCurvesfile. A page is a read and adds nothing to the document.drawingViewwithsaveCurves, or withoutmode: "drawing", isE_INVALID_ARGS, and so is an unknown view (the error lists the views).imported.dxf.bodies[].outlineLayersis unchanged on the wire; the executor now takes it from the bodies alone.
Also on the surface since 0.15.0
cad_drill_holeandcad_hole_patternwithtype: "tapped"take ASME B1.1 Unified sizes (UNC / UNF #0 to 1", plus 1"-32 UN):"1/4-20 UNC","1/4-20","#10-32 UNF","1-32 UN", with an optional"-2B"class. The bore is the 75 % thread tap drill (1/4-20 → #7, ⌀5.105 mm), and the callout reads like1/4-20 UNC-2B ↧6.4, TAP DRILL #7 ↧8. Such a size used to be refused as an unrecognised thread size.cad_ribwithoutattachZattaches to the base's top face, not z = 0, which used to refuse a rib on a plate. Itsanalytic.volumeis now the whole resulting solid (base + rib), not the rib alone, so the mesh cross-check no longer reports a falsemesh_volume_heavyon a good body.cad_print/cad_print_statusrefuse a saved printer on a transport they cannot drive (anything but Bambu today) withE_PRECONDITION,details.code: "transport_not_implemented", naming the transport. A non-Bambu printer used to fall through into the Bambu LAN path and be refused for its file format.- Volume conservation for a union or subtract with a sphere is now checked over the intersection only. A correct result on a large, thin operand is no longer refused as
volume_not_conservedand sent down the slow general path. cad_server_infostays inside its budget: the perf section is capped at 4 KiB. It used to grow with the 512-row history until the reply spilled, and the spill then failed the tool's own output-schema check (/server must have required property 'server'). A spilled reply is no longer checked against a tool's output schema.- A declared per-entity tolerance now lifts to what it bounds, so tol(vertex) ≥ tol(edge) ≥ tol(face) holds for declared tolerances as it does for derived ones. A coincidence the declaration admits is no longer refused.
- The blend-overflow keep-surface refusal names the heal codes that actually apply (
healface_neighbours_not_planar,healface_seam_not_ruled,healface_seam_no_intersection), nothealface_neighbours_parallel. ItsreasonCodeis unchanged. - A smaller listing. Descriptions are capped at 393 B (was 420 B) behind a shorter
cad_helppointer, with no sentence lost, and shared fields have short briefs. A cut description no longer ends at "e.g." or inside a parenthesis. - NativeCAD also ships as a Claude chat plugin:
nativecad-mcp-0.16.0.plugincarries the samedist/server.min.jsas the.mcpb, plus thenativecadskill. - The browser modeller opens a
.pdfstraight into "Read as a drawing", with a Page box, and reads a DXF as a drawing too. That ships with the site, not in this package.
tools/list at 159 tools is 372,089 B (363.4 KiB), down from 382,298 B. That is inside the 385,024 B (376 KiB) line, and tool-list-budget is green.
NativeCAD 0.15.x
v0.15.0 “Svetlanov”
159 tools — seventeen added since 0.14.1; nothing removed or renamed. Command schema 1.27.0 → 1.41.0 (97 document operations, was 81), additive throughout. The headline is DXF: cad_import reads a .dxf. A laser-cut drawing becomes a 2D sketch, or, given the sheet gauge, an exact flat part named from its layers that cad_export_body writes out as STEP. An engineering drawing (mode: "drawing") is split into its views. docs/DXF_IMPORT.md is the reference.
cad_import reads DXF
- One option decides what lands. Without
thicknessyou get a 2D sketch in world XY. Every arc is still an arc, asketchhandle is minted over MCP, andcad_extrude_profile { profile: { sketch } }sweeps it exactly. Withthickness: <mm>you get a flat part: each outer loop minus its holes, extruded +Z from z = 0, one exact solid per profile, largest area first. A hole is a real cylinder. An ELLIPSE, SPLINE or spline-fit POLYLINE loop becomes a rational B-spline wall whose volume matches the closed form. Block INSERTs and MINSERT arrays are expanded, mirrored and non-uniformly scaled ones included. - Bodies are named from the drawing. A body's name is the layer of its outline loop; layer
0falls back to the file stem, and repeats get_2,_3. That name is the STEPPRODUCTnamecad_export_body/cad_savewrite, whatcad_list_model_statelists underparts, and whatcad_assembly_bomgroups by. It rides on the reply asname,imported.namesandimported.dxf.bodies[].name.imported.dxf.bodies[].outlineLayerscarries the outline's layer(s), taken from the import's own chain rather than a second read of the file. - Options.
unitsoverrides$INSUNITS; a file that declares nothing is read as mm and says so (sourceUnits.assumed). There are alsolayers/excludeLayersandgapTol(default 0.01 mm). On a non-DXF path,thickness,layers,excludeLayersandgapTolare refused by name, never silently ignored. mode: "drawing"builds no geometry. It splits the sheet into views, frame, title block (fields read first from ATTRIB tags) and annotation, names each view's projection (first or third angle), and scales each view on its own.saveCurves: <path>writes every view's curves to a JSON file and reports it ondrawing.curves. A drawing kept in a paper-space layout is read through its VIEWPORTs, with per-entity linetypes and ATTRIB values kept.space("auto"by default, or"model"/"paper") andlayout(a layout name, matched case-insensitively) choose the sheet instead of leaving it to the file (schema 1.41.0). The choice is reported ondrawing.space;space.reasonand the paper-space warnings name those two arguments. The title block carries its raw ATTRIB records astitleBlock.attributes({tag, value, field, invisible, insert}, the first 50), withattributeCountandattributesTruncated. A value over 200 characters is clipped and flaggedvalueTruncated.- Nothing is dropped silently.
entry.imported.dxfreports every layer, the entity types that were skipped, the profiles, the healed gaps, open chains with their gaps, withheld loops, and the drawing's dimensions / texts / ATTRIB values / hatches as data. If at least one closed profile exists, an open chain is a warning, not a failure. - New refusal vocabulary, each on
details.reasonCodewith a remedy:DXF_NO_CLOSED_PROFILE(the detail names the gap that would close the outline, e.g.retry with gapTol >= 0.5 (mm)),DXF_EXTRUDE_FAILED,DXF_UNREADABLE,DXF_BINARY,DXF_IS_DWG,DXF_NOT_DXFandDXF_NO_VIEWS. Two more,DXF_UNKNOWN_LAYOUTandDXF_BAD_SPACE, come back asE_INVALID_ARGS(the argument is wrong, not the geometry), with the file's layouts in the hint. cad_importstill needs the licence key, as every import has since an earlier release: an unlicensed server refuses it withE_LICENSE_REQUIREDbefore reading the file.- The browser modeller opens a
.dxfthrough the same importer. Its dialog shows$INSUNITSand a preview of the drawing and its detected profiles. That ships with the site, not in this package.
Inline line/arc loops for cad_extrude_profile / cad_pad
A profile loop can now be written inline as { start, segments: [ {to} | {to, center, ccw} ] }, a third PROFILE_LOOP form (schema 1.39.0). It goes through the same self-intersection and orientation checks as the other two forms. New refusals: PROFILE_ARC_LOOP_SHAPE, PROFILE_ARC_BAD_CENTER, PROFILE_ARC_ZERO_SEGMENT. The DXF reader emits this form, so a filleted or scalloped outline extrudes exactly (profile.exact: true).
Also on the surface since 0.14.1
cad_defeature: removes every hole, boss, fillet and chamfer under a given size, or suppresses the blend networks of the given faces.cad_feature_diagnose/cad_feature_suppress: diagnose reports every feature's state (ok / warning / error / suppressed / suppressedByFailure / stale) and, for one named feature, its refusal and the feature that actually broke. Suppress is the eye toggle that lets the rest of the tree build again.cad_offset_faces/cad_replace_face: direct editing. Offset a set of faces together by one distance, or swap one face's surface; either way the neighbours are re-trimmed.cad_ruled_surface/cad_offset_surface/cad_flatten_surface: ruled sheets, offset copies of B-spline faces as sheets, and a curved face developed flat with the stretch it costs measured.cad_lattice_fill: a strut or TPMS lattice inside a solid, under an optional skin, with volume, density and cell/strut counts.cad_topopt: topology optimisation to a volume fraction, over several load cases at once.cad_nest: nests bodies on a Bambu X1C plate or in an SLS/MJF powder volume, proves clearance on the real triangles, and writes one 3MF.cad_support: tree, grid or linear breakaway supports for non-Bambu FDM and resin, proven clear of the part.cad_printer: a saved-printer list, so every print tool can takeprinter: "<name>"instead of an ip / serial / access code on each call.cad_job_status/cad_job_result/cad_job_cancel: async jobs for long ops, starting withcad_mesh_to_brepjobMode: true. A cancelled job is rolled back, andcad_job_resultthen answersE_CANCELLED.cad_gpu: manages the optional WebGPU addon (dawn.node). Nothing GPU-related is installed unless you ask for it.
tools/list at 159 tools is 382,298 B (373.3 KiB). That is inside the 388 KiB line and the 400 KiB ceiling, and tool-list-budget is green.
Fixed reported issues
- Welded tube and box frames. A plate on an inclined tube now stays analytic (378 → 25 faces, worker ~500 → ~175 MB), and so does a cross tube butted onto an extruded spar.
{convexity: "concave", all: true}now reaches tube-saddle edges, and the r = 3 weld fillet on the butted cross tube builds. The tube-sweep, elbow, fillet-radius and mitre parts of the report are filed as follow-up work.
NativeCAD 0.14.x
v0.14.1 “Sawallisch”
sha256 nativecad-mcp-0.14.1.mcpb cb3d2450231be3da8c79c36045015c1592119b26ee3891582f34efe64e51113esha256 nativecad-mcp-0.14.1.tgz 49c01136cc3c6db79d3e526bb65f3fb21b71e00ec6e7ec5b7e3cfa2ccc97a919142 tools — two added since 0.14.0: cad_gear and cad_print_in_place_check; nothing removed or renamed. The point release exists so the .mcpb a person installs can be handed to someone with an X1 Carbon and nothing else: the unlicensed tier now BUILDS (below), tools/list is back under its ceiling, every x1c_* DFM rule carries a remedy, and the walkthrough that person reads ships with the release — docs/PRINT_WITH_CLAUDE.md, rendered at web/print.html.
The neighbour can install it without a terminal
- Two OPTIONAL install-dialog fields on the
.mcpb: Bambu Studio (bambu_studio_path→BAMBU_STUDIO_BIN) and Bambu Studio resources folder (bambu_studio_resources→BAMBU_STUDIO_RESOURCES). Both are fine blank: a blank field, or the unexpanded${user_config.…}template a host may hand over for one (the shape), reads as UNSET and the slicer search falls through toPATHand the well-known install locations exactly as before.server.jsondeclares the same two variables for a registry install. docs/PRINT_WITH_CLAUDE.md— checklist, install, three first prompts, whatcad_print_prepdoes stage by stage, how to readmethod:"slice"vs"estimate"and adfm_blockedrefusal, printing through Bambu Studio or Bambu Connect (LAN mode is explicitly OUT of the guide: the.mcpbdoes not shippackages/print, somode:"lan"refusesprint_deps_missingthere andmode:"connect"is the path), a troubleshooting table keyed on the refusal codes.
tools/list is back under budget
The four budget assertions 0.14.0 carried red are green at 142 tools: packages/mcp/test/tool-list-budget.test.js and cadsb-1273.test.js pass. Field prose that only restated the keyword beside it is no longer published, dedupeSchema folds the most profitable repeated subtree (cad_face_blend 12,307 → 4,848 B with no prose touched), cad_export_body's 15-byte field-description stubs are gone (18,734 → 13,157 B, back inside its 16 KiB exception), and the per-tool budget tightens itself over 3 KiB / 6 KiB of schema. Nothing a validator reads changed and nothing left the surface. The total line is re-based to 388 KiB — 360 KiB is not reachable at this tool count without deleting published arguments, and the ticket records the measured floor — with the 400 KiB absolute ceiling unchanged.
Every x1c_* verdict has a remedy
ops/verdictRemedies.js now answers for the twenty single-body x1c_* rules the fdm_x1c pack measures (walls, pins, overhangs, bridges, text, elephant foot, the exclusion corner …) and for the six x1c_pip_* codes cad_print_in_place_check introduces (gap, fuse, zgap, escape, interference, ratio). docs/VERDICT_REMEDIES.md lists them; tools/design_loops/fdm_x1c.js converges on those remedies rather than on the two it had.
Also on the surface since 0.14.0
cad_gear: involute spur / helical / herringbone gears and a planetary set as an executor op (ops/gear.js,ops/helicalSweep.js), in the command schema with its validator;docs/GEARS.md.cad_print_in_place_check: sweeps a gear train through its motion and measures every body pair against the X1C's printed-clearance floor, the fusion floor and the one-layer Z gap BEFORE a slice, and says whether a body can escape (the part was never print-in-place).- The
fdm_x1cknowledge pack is published:data/dfm/packs/fdm-x1c.json→dist/fdm-x1c.{core,standard,full}.json, and onweb/dfm-knowledge.html(29 packs, 1,571 entries). - executor report assembler /
ops/transform.js, pick-anywhere placement invariants I1–I9, a flush-at-both-ends through cut stays analytic inops/boolean.js, piping pack re-pinned, browser build-orientation panel, browser padstack panel, Labs pendant part 3.
The unlicensed tier is now EXPORT-gated, not verb-gated
A call that used to refuse now succeeds. An unlicensed server previously ran ten tools and refused the other 130 with E_LICENSE_REQUIRED — 7% of the surface. It now runs 130 and refuses ten: cad_export_body, cad_save, cad_restore, cad_versions, cad_import, cad_assembly_import, cad_mesh_to_brep, cad_mesh_payload, cad_quote_payload, cad_quote_submit. Modelling, query, measurement, DFM, FEA and the evaluation loop are open with no key. The boundary is the one contest mode has enforced since an earlier release — build anything, take nothing away — promoted to the default so there is one gate vocabulary rather than two.
What a caller keys off, and what changed under it:
E_LICENSE_REQUIREDstill means the same thing and is still raised before argument validation. The tools it can name are now exactly the ten above.details.allowedWithoutKeyis GONE, replaced bydetails.deniedWithoutKey. A caller reading the old field getsundefined. The refused set is the short one now, and it is the one an agent that just hit the refusal needs.cad_server_info'sgateblock carriesdeniedWithoutKeyfor the same reason, andgate.statementno longer says "only the evaluation loop is available" — a model that plans around that sentence will not try to build.cad_license statusreturnsgatedWithoutKeywhere it returnedevalTools.gatedstill means "this server is unregistered".licensing.EVAL_ALLOWLISTis replaced bylicensing.UNLICENSED_DENYLIST;licensing.gateInForce()is new (isGated(null)still answers the same question, but the readouts now ask for it by name).CONTEST_DENYLISTis unchanged — same twelve names, same order — but is now derived fromUNLICENSED_DENYLISTpluscad_eval_packet/cad_eval_share. docs/CONTEST_CONTRACT.md §7 did not move.
v0.14.0 “Rozhdestvensky”
140 tools — ten added since 0.13.0: cad_build_orientation, cad_slice, cad_print, cad_print_status, cad_print_prep (a new print subsystem), cad_face_blend, cad_sheet_feature, cad_sheet_miter, cad_padstack, cad_trace_section; nothing removed or renamed. The theme is the Bambu Lab X1 Carbon: a body built in a session becomes a .gcode.3mf, and — on a printer whose owner has turned Developer Mode on — a job running on the plate. Command schema 1.27.0, 80 document operations, additive throughout.
The print pipeline
Six stages, each its own tool, and one tool that runs all six. docs/PRINT_PIPELINE.md is the reference; what a caller keys off:
cad_dfm_checkpackfdm_x1c— 23 sourced rules over 14 concerns read off the X1C's own published profile numbers: the 256x256x250 mm envelope including the 18x28 mm front-left exclusion corner, minimum wall, overhang, bridge, free-standing pin, embossed text, elephant foot, Z anisotropy, multicolour purge cost. Rule codes arex1c_*alongside the genericmin_wall_thickness/max_overhang_angle; the pack auto-orients by default before it judges.cad_build_orientation— ranks plate orientations by a candidate sweep over the same pack's analysis, six normalised penalty terms, exclusion-corner fit; returns a row-major 4x4 transform the Bambu 3MF writer consumes directly.cad_export_body format:"3mf-bambu"— a Bambu project, not a bare mesh:Metadata/model_settings.config,Metadata/project_settings.config, thumbnails, per-body filament slots mapped by colour. Bambu Studio opens it with no "not from Bambu Lab" dialog.3mf(the plain one) is unchanged.cad_slice— spawns a user-installed Bambu Studio / OrcaSlicer (BAMBU_STUDIO_BIN, thenPATH, then well-known install locations) and hands back the.gcode.3mfplus the slicer's ownpredictionSandweightG. Always readmethod:"slice"is measured,"estimate"isops/printEstimate.js— a{low, high}band, no file,refusalcarryingslicer_missing.methodis required by the output schema precisely so an estimate can never be read as a measurement.cad_print/cad_print_status—mode:"lan"pins the printer's self-signed certificate against its serial (TOFU, in~/.nativecad/printers.jsonorCADSOLVER_PRINTER_TRUST, never in the checkout), reads the AMS overmqtts://…:8883, maps each filament slot to a tray before the upload, uploads over implicit-TLS FTP on 990 (not AUTH TLS on 21), publishesproject_filewith the uploaded file's md5 and theams_mapping, and waits for the printer itself to sayPREPARE/RUNNING—started:trueis the machine's word.mode:"connect"returns abambu-connect://import-file?…link instead and is stampedstarted:null, so a Connect link is never read as a started print. IP / serial / access code come from the call or fromCADSOLVER_PRINTER_IP/CADSOLVER_PRINTER_SERIAL/CADSOLVER_PRINTER_ACCESS_CODEand from nowhere else; the serial is masked in the report and the access code is never echoed.cad_print_prep— stages 1–5 in one call and one report, each stage still dispatched through the registry. It refuses at the first failing stage under that stage's own code, withdetails.stagenaming where it stopped;force:truecarries a DFM block,orient:"keep"carries an orientation refusal,slicer_missingfalls back to the estimator rather than failing.
packages/print (the mqtt + basic-ftp adapter) is the only part of this repository with npm dependencies, is loaded lazily, and is deliberately not in the .mcpb: in a packaged install mode:"lan" refuses with print_deps_missing naming npm install --prefix packages/print, and mode:"connect" keeps working.
New refusal codes
Every one carries its code in details.code.
cad_slice:slicer_missing(E_PRECONDITION, or the estimate),slicer_failed(E_INTERNAL, with the exit code and the stderr tail),slicer_object_outside(E_GEOMETRY),slicer_profile_mismatch(E_PRECONDITION).cad_print/cad_print_status:printer_target_missing(E_INVALID_ARGS),printer_unreachable,printer_auth_failed,printer_not_developer_mode,print_ams_slot_unmapped,print_file_rejected,print_deps_missing,printer_cert_mismatch(allE_PRECONDITION). Each names the Bambu Connect fallback as the way to print anyway.cad_print_prep:dfm_blockedwithblockedBynaming every blocking rule, plus whatever the failing stage refused with anddetails.connectUrlwhen the send stage is what failed.- Booleans:
tangent_contact_pinch— a nested tangency is now recorded rather than chained through, and the pinch is refused by name instead of producing a body with a zero-thickness contact. cad_face_blend: 23 namedface-blend-*reasons (face-blend-no-chain,face-blend-chain-ambiguous,face-blend-radius-unsolvable,face-blend-not-prismatic,face-blend-hold-line-unsupported, …) — a full round is solved, not chosen, and an unsolvable one says which of the 23 it is.cad_padstack: 19padstack_*reasons, includingpadstack_span_not_copper,padstack_pad_off_via_span,padstack_pad_smaller_than_drill,padstack_antipad_smaller_than_hole,padstack_microvia_not_adjacent,padstack_backdrill_ends_in_copper/_exceeds_span/_not_larger,padstack_stub_exceeds_max,padstack_plating_exceeds_drill,padstack_vias_overlap,padstack_volume_audit_failed.cad_trace_section:trace_section_bad_route,trace_section_bad_parameter,trace_section_overlapping_conductors,trace_section_mask_thicker_than_gap,trace_section_volume_audit_failed.- Shaft keyways:
keyway_unreadable— a keyway the B-rep cannot be read back off is refused by name rather than guessed at — beside the existingkeyway_overlap,keyway_wider_than_shaft,keyway_reaches_axis,keyway_floor_outside_shaft.
topoMap on every modeling result
Every executor result now carries one map of what the operation did to the topology it was handed: { modified: [[oldId, newId], …], generated: [newId, …], deleted: [oldId, …], counts, source, derived }, keyed on STEP entity ids of faces and edge curves. An input id still present unchanged appears in none of the three lists and is counted in counts.unchanged — the OCCT / FreeCAD convention, and the reason the common case costs nothing. source says whether the operator handed its own correspondence up (operator — blends, chamfers and the booleans do) or the map was derived (derived-topology). Additive: nothing that existed on a result moved or changed meaning.
PCB: cad_padstack and cad_trace_section
cad_padstackbuilds analytic PTH / microvia solids from a stackup, and emits the via bore column as real bodies — fill, air void, back-drill void. A pad or antipad on a layer the via never reaches is refused. This release finishes the solid model: a meniscus fill top (a spherical cap on theriring, with a closed-form cap volume), resin bodies in the etched-away copper that share the pad and antipad walls, cap plating trimmed against same-side solder mask (and measured gone, not assumed), and split heights that agree across mixed vias because the bodies share edges, not merely coincident surfaces.BrepMassPropertiescloses a.F.-oriented wrapping face on the correct pole, which is what made those volume audits trustworthy.cad_trace_sectionturns a trace cross-section into solids; this release sweeps it along a route (volume by Pappus, per body), makes neighbouring bodies share their interface faces by identity rather than by coincidence, and adds conformal plating in which paired conductors may differ intFoil/tPlating.
Also on the surface since 0.13.0
- Sheet metal.
cad_sheet_feature(hem, jog, sketched bend, tab, cross-break, vent) andcad_sheet_miter(plane–plane miter). Bend methods beyond K-factor on the sheet verbs:bendAllowance,bendDeduction,bendTable, andgauge+materialresolved throughdata/sheetmetal/gauge-table.json. Sheet-metal overrides are scoped to the document andautoDimensionids to the call, so one caller's override no longer leaks into another's session. - Chamfer. Asymmetric (two distances) and distance–angle chamfers.
- Blend networks are on by default (after shipped them behind a flag at 0.13.0): a whole-selection blend is classified as a network and solved as one.
CADSOLVER_BLEND_NETWORK=0, ornetworkSolver:falseon a single call, is the opt-out. cad_wind_tunnelgainssizing— a refinement zone with the uniform-vs-refined Cd delta stated.- Shafts.
surface:'bore'on the shaft verbs, with shoulder and centre drill built on a closed meridian loop; an internal keyseat in a bore, andrecoverKeywaysnaming a non-keyway axis-parallel face instead of mistaking it for one;_revolveProfilespruned like_shaftProfiles. - Assembly.
cad_assembly_contactsno longer reports a false interference between coaxial cylinders — two parts on the same axis with clearance between them read as clear. - Kernel, under the surface: Shewchuk exact predicates wired into
ops/polygonBoolean.js; a.kicad_pcbreader into the neutral layout model;healFace's seam solve widened to plane/cylinder, cylinder/cylinder and plane/cone with arc re-mint;ops/envFlag.jscentralisesCADSOLVER_*gate parsing andIncrementalEval.tessKeykeys the raw environment throughenvSig, so a cache entry can no longer outlive the flags that produced it;RENDER.writePNGis deterministic — measured, pinned, then tested. cad_fea_run.meshQuality— the boundary re-cut and tet-quality pass on the public tool, no schema default.- Build and release gates (no runtime effect, but they are why this build can be trusted): the bundle-freshness gate re-hashes every file esbuild bundled rather than a recorded subset,
commands/schema/generated/command-schema.v1.jsonhas a staleness gate of its own, andtools/worker.shrepairs an unrepaired gitdir.
Known reds carried
packages/mcp/test/tool-list-budget.test.js is red in four of its budget assertions at 140 tools. tools/list measures 431,283 B across 140 tools — 421.2 KiB, over both the 360 KiB total line and the 400 KiB absolute ceiling; cad_export_body is 18,734 B, over the 16 KiB backstop its documented exception carries; and cad_face_blend at 12,307 B is the first non-exception tool past the 12 KB top-tool line. What still holds: 3,080.6 B/tool, a 39.9 % reduction off the 5,128 B/tool baseline (the growth-proof per-tool gate), the 3.3 KB/tool line, every tool description inside the 420 B cap (max 420 B), the cad_dfm_check ajv round-trip, and the cad_help round-trip block. Nothing was dropped from the surface to make the number smaller; the fix belongs to follow-up work.
What a caller might key off
- Ten new tools; nothing removed or renamed.
cad_server_info's subsystem list gainsprintand no longer needs anotherbucket. cad_export_bodyacceptsformat:"3mf-bambu"alongside3mf.- Every modeling result carries
topoMap(additive). - A boolean across a nested tangency refuses
tangent_contact_pinchinstead of building a pinched body. - A whole-selection blend is solved as a network by default — set
networkSolver:false(orCADSOLVER_BLEND_NETWORK=0) for the pre-0.14.0 per-edge behaviour. cad_assembly_contactsno longer reports interference for coaxial cylinders with clearance — a caller that filtered that false positive out can stop.- Sheet-metal overrides are document-scoped and
autoDimensionids call-scoped; a caller that relied on an override outliving its document must set it again. cad_slice'smethodfield distinguishes a measured slice from a modelled estimate and is required by the output schema; a caller that reports a print time must read it.mode:"connect"answers carrystarted:null— never treat a Connect link as a started print.- Command schema 1.27.0, 80 document operations (
commands/schema/generated/command-schema.v1.json): additive fields only.
NativeCAD 0.13.x
v0.13.0 “Rostropovich”
sha256 nativecad-mcp-0.13.0.mcpb 7a911841be672c6640191609441d62a1b4cb0e977a472b1f17cc645b2a5f3434sha256 nativecad-mcp-0.13.0.tgz af5f7f7e4f7f95dc75443ac43e7e20cb385b5b2ee3e998d536732560dda8fff1130 tools — three added since 0.12.4: cad_fea_case, cad_fea_run, cad_modal; nothing removed or renamed. The CalculiX / MYSTRAN finite-element arm ships in the npm tarball and the .mcpb for the first time.
The FEA tools ship
They were the private fea group since an earlier release because the group's preferred solver was a partner's product. This release splits the group: the CalculiX / MYSTRAN arm (the case contract, ops/feaCalculix.js C3D10 decks, ops/feaNastran.js, the CaeExporter writers) ships; the partner-solver adapter and cad_blade_qualify (blade qualification) stay in a private group, pending the commercial agreement. docs/AGENT_FEA_LOOP.md's five-call loop and tools/design_loop.js --instance bracket_fea run from an install with ccx on PATH.
cad_fea_run.solveris no longer an enum."auto"(default) runs every registered solver adapter in order, then CalculiX;"calculix"forces ccx. A name this build does not know isE_INVALID_ARGSwithdetails.reason: "solver_unknown"anddetails.available, never a silent substitution. An edition that carries a further solver adapter registers it under its own name and"auto"tries it first.solver_missinghints name the CalculiX install routes (apt / brew / conda-forge /CADSOLVER_CCX).cad_server_info.privateGroupsno longer listsfeaas withheld on a public boot; only the blade-qualification group is.
Two release gates, fixed
scripts/stage-kernel.js's dynamic-require scan matched any identifier that merely ended inrequire; the wave-3 landing'sTolerance.jsguard_require(scope, fn)tripped it and no release build after 4d5c4097 could produce a bundle. The scan now matches only the loader's ownrequire(.- With the three FEA tools public,
tools/listwas 354 KB against the 340 KiB line.cad_fea_case.rotoris published by reference (the full shape is oncad_modal;ops/feaCase.jsvalidates every field either way — a static case with a rotor is stillrotor_requires_modal),fatigueCorrectionis oneadditionalPropertiesshape (an unknown factor is stillmaterial_invalid, by name), andcad_fea_case/cad_modaltake the registry's 25 B field budget — full prose stays reachable throughcad_help. 350.9 KB; the total line is rebased to 360 KiB at 130 tools, the 400 KiB absolute ceiling unchanged.
Also on the surface since 0.12.4
CADSOLVER_TOOLSET=full|core+expand|verb-payloadis enforced at listing and dispatch (E_TOOL_NOT_AVAILABLE;cad_helpopens a tool for the session).server.jsonfor the official MCP registry, with a version-drift gate.- Per-client journal call lines and pending slots;
restoreDocumentclaims the default document only on crash restore;of:"all"is scoped to the caller. - This release descopes the cross-solver MODAL agreement band.
- Live-body quotes no longer throw into
file_unavailable. cad_fastener_auditnames stated-but-unmodelled fixings (unfastened[]).
Known reds carried
cadsf-1296.test.js (blade-qualification fixture predates the rotor.vaneCount requirement — private group) and cadsf-1448.test.js (a modeCount description regex written for the 100 B field budget lowered to 50). Both were red at 0.12.4; neither is a shipped behaviour.
NativeCAD 0.12.x
v0.12.4 “Masur”
sha256 nativecad-mcp-0.12.4.mcpb 1dd9a4879269b1370c421e7051a54501becde8edf3ff8e4217ac6626ea8777c8sha256 nativecad-mcp-0.12.4.tgz 9658319cdba251d6da9ad96b8dd813fc161a9c7f1cda2bdb3bc5305558f44e89127 tools — one added since 0.12.3, cad_weld_schedule (landed after the 0.12.3 build); nothing removed or renamed. The theme is that the MCP surface reports what it actually did, and the known red 0.12.2 and 0.12.3 both shipped with is closed.
A partial batch comes back partial
cad_batch with stopOnError:false used to answer a failed op with an ERROR envelope that discarded every other op's result — the one setting whose entire purpose is "keep going" threw away what kept going. It now returns ok:false carrying every op's inline result or error, plus a skipped entry for an op that referenced a failed op's name. batch.test.js 66/66.
cad_gap_registry ships at all
Every .mcpb and .tgz shipped so far answered E_PRECONDITION for cad_gap_registry: registry.js located data/gaps/registry.json through a private __dirname escape that no packaged layout satisfies. It goes through kernelAsset() now, so stage-kernel.js and bundle-server.mjs stage and embed it like every other data file — 4 verbatim assets, was 3. The tool works in a packaged build for the first time.
cad_list_model_state names consumed inputs
Additive: handles.retired.solid and a consumed[] of {handle, kind, consumedBy:{op, opIndex}, stale}, so a caller can tell a handle that was eaten by a boolean from one that never existed. SessionStore.peekValue() keeps a listing from warming the LRU. cadsb-529.test.js 55 → 75.
An exact B-rep centroid and inertia
cad_mass_properties carries a centroid and inertia integrated over the same analytic faces as the exact volume — closed forms to 1e-15 on the box, the off-origin box, the cylinder, the sphere, the cone and the torus, where the tessellated inertia was 0.3–0.6 % out. Published under the agreement guard with per-field centroidSource / inertiaSource / meshCentroid / exactCentroidRefusal, and volumeSource now reaches the payload. 84 assertions.
The known red is closed
cad_license's offline, no-network posture moves into the first sentence-and-a-half of its description, so it fits inside the 420 B tools/list description cap. license.test.js is green again at 145 (201 with the gate file). 0.12.2 and 0.12.3 both shipped naming this red; 0.12.4 does not.
Kernel
- A union whose tool is contained in the base (B ⊆ A) answers as a copy of the containing operand rather than a contact refusal.
- The coplanar-continuation run-out's free end builds, and the free-end/split-rail pair refuses by name: 3 of the 6 rows build, was 1.
- The equal-radius "ear" regime is analytic — 12 faces in ~25 ms where the general engine gave 907–1205
PLANEfaces in 5–7 s. - A union across a tangent circle is
non_connected_resultand a subtract across one isno_material_removed, both naming the contact; the measured shared volume is exactly 0. - A blend fit probe with one unreadable end no longer discards the whole pick's overruns —
LED_1812at r = 0.045 goes dressed 24 → 28 and blend-result-not-watertight 3 → 0. - A root fillet running out onto a cylindrical wall the body carries as several faces of one cylinder is named
blend-sel-cyl-wall-runout-splitwith its census. - Mass-property no-ops are no-ops again;
surfaceAreacomes off the same B-rep asvolume, and an inverted shell no longer reports negative area. cad_sheet_foldandcad_sheet_boxself-gate on thescs_bendDFM pack they used to build past silently, with a material-independentmin_bend_radiusrule.- The bundle-freshness hash is a function of the commit, not the host.
What a caller might key off:
cad_batchwithstopOnError:falseno longer returns anERRORenvelope on partial failure — it returnsok:falsewith per-opresult/error/skipped. Code that read the envelope and gave up must read the entries.cad_gap_registryanswers in a packaged build instead ofE_PRECONDITION.cad_list_model_stategainshandles.retired.solidandconsumed[](additive; nothing removed).cad_mass_propertiescentroid and inertia are exact on analytic bodies, withcentroidSource/inertiaSourcesaying which path produced them;volumeSourceis now present on the payload.cad_sheet_fold/cad_sheet_boxrefuse a bend tighter than the DFM pack'smin_bend_radiusinstead of building it.- New tool:
cad_weld_schedule. Nothing removed or renamed.
v0.12.3 “Marriner”
sha256 nativecad-mcp-0.12.3.mcpb 167dedf4905a2938e6dc986c0abe2c8fd4ef2e5cfa5e654361f7dc948276117dsha256 nativecad-mcp-0.12.3.tgz 1ac423fb2efaa1bc6783eac1dac636c9361dddd0ed5e9bba0b42eb3ed3c9fe9cBumped on 2026-09-19 with the boolean-pair entry below and never cut; the release that carries this number was built on 2026-09-20 and also ships the day's work, summarised first.
A saddle corner whose concave edge is not picked now BUILDS
A cad_fillet_edges whole-body call, and any selection that picks the two CONVEX edges meeting a trihedral saddle vertex while leaving the CONCAVE one out, used to refuse blend-sel-vertex-saddle ("edge #N meets this saddle vertex but was not selected — the setback patch is bounded by ALL THREE strips' end arcs"). A blanket convex fillet can never select a concave edge, so on a mixed body that was every saddle corner. It builds now: the two strips lie on opposite sides of the concave crease and never overlap in material, so each simply ends against the other edge's support plane, the crease splits at the one point both wall rails reach (its point at distance r from the shared cap face), and the wall plane below that point carries a separate planar sliver. Nothing is set back — the volume removed is both strips in full. Measured: the L-block's blanket fillet goes from 15 of 18 edges to 17 of 18 (only the reentrant edge is skipped), its 2-of-3 pick at the notch vertex is watertight with a clean checkFull and removes 2·(1−π/4)r²·6 to 2.9e-5 relative; on 0001282f6783_LED_1812_4532Metric.step the blend-sel-vertex-saddle row of the whole-body histogram goes 16 → 0, and on 007bc1f1b1ea_PinSocket_1x05_P1.27mm_Vertical.step 4 → 0 (r = 1% of max extent, this tree).
Unchanged: a pick that takes ALL THREE edges at the vertex is still the setback family; a {corner, mid} variable-radius law has no single r to pin the split with, so the corner is declined by name and the blanket law refusal is byte-identical; single-edge results are unchanged; and cad_chamfer_edges has no such corner construction, so its blanket prune still drops one convex edge per saddle vertex.
Refusals name themselves, and so do kernel deaths
786 of the 1,202 previously unnamed refusals now carry a structured error.details.reasonCode. 418 of them were a leak, not a missing vocabulary: ops/blend.js, shell.js, boolean.js and chamfer.js already stamped .reasonCode at every throw, but the E_GEOMETRY envelope in tools/modeling was rebuilt from the prose and dropped it. cad_server_info publishes the vocabulary under reasonCodes; tools/refusal_reason_census.js keeps the count re-measurable. No message text changed.
Every kernel-worker death is now one of five codes — kernel-worker-module-missing (with details.missingModule), kernel-worker-exited, kernel-worker-shutdown-in-flight, kernel-worker-killed, kernel-worker-threw — carried on E_KERNEL_CRASHED as reasonCode and registered alongside the error codes. Module-missing and shutdown-in-flight are recoverable:false with their own hint. Four tool layers that used to overwrite the details bag with recoverable:true now merge into it.
The diagnostics payload family's ceiling moves from 48 KiB to 64 KiB (packages/mcp/src/budgets.js, docs/PAYLOAD_BUDGETS.md): cad_server_info at section:"all" with an open contest scope measured 54,987 B once the reason-code vocabulary was published, and the frame — not the field — was the binding constraint. The published-shape matrix workflow also installs the repo root's deps so its npm pack can build dist/server.min.js at all.
Six comparison thresholds (15°, 35°, 5°, 1° sines/cosines in DFM off-axis faces, crease splitting, coplanar-edge and ruled-along tests, smooth clustering) are literal constants instead of run-time transcendentals, so the same body classifies identically under V8 and JavaScriptCore.
What a caller might key off:
E_GEOMETRYrefusals from blend / shell / boolean / chamfer that used to arrive with prose only now carrydetails.reasonCode; code that fell back to substring-matching the message can switch.E_KERNEL_CRASHEDalways has areasonCode; a module-missing death is no longer reportedrecoverable:true.- A
cad_server_infodocument up to 64 KiB is a normal answer, not anE_LIMIT.
Kernel: the lattice builds, and three long-standing refusals move
The lattice scenario builds and the rounded cube reaches the general engine inside its meshFallback budget. A lathe-turned body whose wall is authored as a line-swept SURFACE_OF_REVOLUTION is recognised as the cylinder or cone it is, so it is a solid of revolution to blendRevolved again (the fillet itself still waits on). A multi-turn helical ribbon is no longer mistaken for a trimmed full-turn band by the tessellator — the thumb-screw thread's repair skin drops from 33,016 mm² to 160 mm² (flanks in). Fillet corner ends where one wall is a cylinder now get a construction instead of "a wall is not a PLANE" (they stop at the next named clause).
Four more boolean pairs answer exactly: the countersink, the O-ring groove, the ball end — and two silent lies
The sweep measured 30 boolean scenarios and ranked what was missing. The top three were not exotic — a countersink, a ball end and an O-ring groove — and all three are now exact, along with the two cases where a boolean returned a clean-looking answer that was not one.
An O-ring groove (a torus subtracted from a cylinder) was refused outright: the general engine's validity gate rolled back on 374–418 open_edge / non_manifold_vertex findings and reported unsupported_overlap. New ops/torusGrooveBool.js answers it in closed form: 8 analytic faces (2 plane, 4 cylindrical, 2 toroidal), the removed volume matching an independent quadrature oracle to 1e-9, checkFull clean, and the general engine never reached. It refuses by name where the shape stops being a groove — torusgroove_breaks_out when the groove is deeper than the wall is thick.
A CONE tool facetted on subtract: a countersink in a plate came back 200 PLANE faces, a bore lead-in chamfer 325, while the same cone UNIONED as a boss stayed analytic at 8. New ops/seatedPocketBool.js recognises the seated pocket — a tool whose mouth lies flat ON the base face and whose widest section IS that opening — and promotes it to a pierced face, but only when the tool straddles nothing anywhere, which is what leaves the tangency refusal untouched. Measured: 8 faces.
Every sphere boolean except subtract-from-a-plane facetted: a ball-end rod (sphere ∪ cylinder) returned 3,503 PLANE faces with the sphere gone, box ∩ sphere 2,125. New ops/sphereBool.js adds the coaxial ball-end rung to union (7 faces) and the clipped-ball rung to intersect — a sphere cut by planes all perpendicular to ONE axis, which is a half-ball, a lens or a spherical segment. A ball clipped on SEVERAL axes (the rounded cube) still refuses sphbox_multi_axis_clip by name and lands on the general engine exactly as before; that is the one remaining gap in the matrix.
A subtract whose tool never touched the base silently republished the base unchanged AND consumed the tool, while the identity block already knew vComplement was 0. It now refuses by name, quoting the measured vol(A) and vol(A∩B).
A boolean that removed everything published a 0-volume, 0-face, 0-edge body flagged watertight: true and ok. Refused as EMPTY_RESULT.
And the words now match the engines. The refusal hints named remedies that did not fit ("intersect analytic bodies" to someone who had just intersected two analytic primitives); the tool descriptions warned callers off a union measured at 29 faces and 0.15 s as costing "thousands of faces, tens of seconds". subtract, union and intersect now carry measured support matrices, and ops/booleanCensus.js routes by (operation, lost surface family) with an ABSENT entry meaning there is no analytic path — said plainly, rather than filled in with advice that costs a caller a turn.
What a caller might key off:
- Three pairs that used to come back
geometryDegradedwithsurfaceTypesLostnamingcone,sphereortorusnow return exact bodies with those families intact and no degradation flags. Code branching onreport.geometryDegradedfor a countersink, a ball-end rod or an O-ring groove will now take the other branch. subtractwith a tool that misses now THROWS instead of returning the base. PassallowNoOp: trueto keep the old behaviour — the tool is then left live and unconsumed rather than silently discarded.- A boolean whose result is empty now throws
EMPTY_RESULTinstead of publishing a 0-face body. - New refusal codes on the exact rungs, all of which let the ladder continue:
torusgroove_breaks_out,sphbox_multi_axis_clip. meshFallbackis a smaller part of the story: the three shapes that most often sent a caller looking for it no longer need it.
v0.12.2 “Mäkelä”
sha256 nativecad-mcp-0.12.2.mcpb a6aec81f2e2f81f69fce6efb8e5cb8edccef7993e34022c692e284ddacd0d3f9sha256 nativecad-mcp-0.12.2.tgz a1752b9e262308fcd53afd2ca46a05bfe004ac3a03d4327525432541cb5e82eeThe equal-radius T-junction is analytic, and tools/list is back under its ceiling
A rod butting INTO another rod of the same radius (a T, as opposed to the cross 0.11.2 made analytic) was refused cyleq_not_through and fell to the general engine: 867 faces with the tool ending on the base's axis, 443 past it, and an outright unsupported_overlap refusal at tangency. Reported live from the web modeller. The boss path could not take it either: at r = R the tool's cap rim always reaches sqrt(s² + R²) ≥ R, so a root is never embedded and the cap always breaks the base's surface near the pinch directions — which is why this is a third construction rather than a parameter of the other two.
New cylCylEqualTee in ops/cylCylEqual.js builds it exactly, every curve a STEP ELLIPSE, CIRCLE or LINE and nothing fitted: the intersection is the same two planar ellipses the cross authors (a tool wall point is outside the base iff |s| > R|sin φ|), and the near cap's plane, being parallel to the base axis, is cut by the base in two straight chords at u = ±√(R²−sEnd²) — chords that are generators of the through cylinder, so the same two lines bound the cap slivers and cut the through wall. Measured: 8 faces (14 edges, 8 vertices) with the tool ending on the axis, 10 faces (20 edges, 12 vertices) short of it, V − E + F = 2, checkFull clean, worst edge tolerance 3.6e-15, volume within 1.5e-4 of a closed form that is itself pinned against its own integral. Either operand order works. The cross is unregressed at 12 faces.
tools/list was 416,675 B against a 409,600 B ceiling and had shipped red since before 0.11.1. Now 335,220 B across 126 tools: the per-description budgets tightened, seven opening sentences rewritten shorter and complete rather than hard-cut, and the boolean tools' duplicated engine-ladder paragraph deduped. The long-form notes remain in cad_help and docs/TOOLS.md, which is where they belong. No tool, parameter or first-sentence meaning was removed.
What a caller might key off:
- The T-junction answers with
regime: 'cyl-cyl-equal-analytic'andcylCyl.tee; it refuses by name —cyleq_tee_end_beyond_axis(the tool's cap past the base's axis but still inside it: two extra wall regions above the pinch, still the general engine's),cyleq_tee_no_overlap(external tangency — contact, not overlap),cyleq_tee_far_end_not_clear,cyleq_tee_is_cross— and the ladder continues on every one. - Tool descriptions are shorter on the wire.
cad_help/describe_toolstill carry the long form; nothing was dropped.
v0.12.1 “Kertész”
sha256 nativecad-mcp-0.12.1.mcpb d86a368e3a42f4d10b4e63ae4af5a70b57c3066f456ff91cb2da3079baf94921sha256 nativecad-mcp-0.12.1.tgz f777d2ecbdf7a3535067f53cb3e3227f8f90f4baf2023821649be6d990a56ccdThe fillet on a cylinder/cylinder saddle BUILDS — a rod through a rod, a boss on a rod, the equal-radius cross
The other half of Chris Wilson's report: with the union analytic, cad_fillet_edges on the junction still refused by name — blend-sel-non-planar-support, "a corner between TWO CURVED supports … a general CHANNEL SURFACE with no exact circle/torus answer". For this pair the offset surfaces are two more coaxial cylinders (R + ρ, r + ρ) on the same perpendicular, intersecting axes, so the rolling ball's spine is the SAME closed-form Steinmetz family as the union edge, and NEW ops/blendSaddleCyl.js builds the canal band from it: contact curves fitted on each wall with a measured, recorded tolerance; the band as a bicubic B-spline surface (one face per tool half) with exact ρ rim arcs; the wall's hole loop and the tool's wall pieces re-trimmed; the quartic edges retire. Concave — a weld fillet at the boss root — so it ADDS material. Measured on Ø10 through Ø20 at ρ 2: 14 faces, checkFull clean, contact curves on their walls to 1.4e-7, band at ρ from its spine to 4e-15, added volume within 2.5 % of a Pappus oracle, STEP round-trips. Works on both crossings of a through rod, one crossing, one picked half (expanded to its loop and reported), the single boss, the swapped-operand union, a tube's outer wall.
The EQUAL-radius cross fillets too: its four elliptic creases meet at two pinch points where the two walls share a tangent plane, so the band closes to a point there — one B-spline face per crease bounded by its two contact curves alone, no rim arcs, the pinch vertices going from valence 8 to
- Both variants report
method: 'saddle'.
What a caller might key off:
cad_fillet_edgeson these edges returnsok: truewithmethod: 'saddle'where it returnedblend-sel-non-planar-support.- New coded refusals when the shape is right but the radius is not:
saddle-runs-off-tool-end(the tool's cap is inside R + ρ),saddle-runs-off-wall(the ±(r + ρ) contact band leaves the wall's end),saddle-runs-onto-seam,saddle-tool-not-capped(a bore bridge between two crossings),saddle-fit-tolerance. Every other pick falls through to the route it had before.
v0.12.0 “Jurowski”
sha256 nativecad-mcp-0.12.0.mcpb 22c4c39c801365ecf10159bad6aa98ee22c381b8323f4ca0a6531da4e0a34e76sha256 nativecad-mcp-0.12.0.tgz 72b8056a9b8d43578020252fe1b1e03a2c2cfbd3aa58d2626be73aed771adff2cad_contest: enter a showcase contest without a licence key
One new tool, on the evaluation allow-list, and a fourth licence state under it. cad_contest start with a ticket from a contest page on featureboard.ai opens a contest scope for the server process: while it is open on an UNLICENSED server (and the contest's entriesCloseAt has not passed) the ordinary modelling tools run, and the twelve tools that would let the build leave the machine for the entrant's own use — cad_export_body, cad_save, cad_restore, cad_versions, cad_import, cad_assembly_import, cad_mesh_to_brep, cad_mesh_payload, cad_quote_payload, cad_quote_submit, cad_eval_packet, cad_eval_share (licensing.CONTEST_DENYLIST) — answer the NEW code E_CONTEST_SCOPE. cad_contest submit (with agree:true) exports the active document to glb in a private temp directory through an internal, Symbol-keyed bypass on the call context, renders a 512 px poster with cad_preview_body, POSTs the docs/CONTEST_CONTRACT.md §4 envelope and deletes the temp files whatever happens — there is deliberately NO offline bundle, unlike cad_eval_share. contests, status and end round it out. A LICENSED server is unaffected by the scope. Wire contract: docs/CONTEST_CONTRACT.md; build notes: docs/CONTEST_TOOL_NOTES.md.
What a caller might key off:
- Three new codes:
E_CONTEST_SCOPE(built-in, raised by the dispatch gate,details.deniedInScopelists the denylist,details.submitWiththe way out),E_CONTEST_REJECTED(the site said no:details.status,details.reasons[],details.phase) andE_CONTEST_OFFLINE(the site could not be reached). All three arerecoverable: trueand incad_server_info'serrorCodes. E_LICENSE_REQUIREDnow carriesdetails.contestHintnaming the contest route. Message, hint and every existing detail are unchanged.cad_server_info.gate.contestis present (unlicensed server, scope open) with the contest, deadline anddeniedInScope;cad_helpmarks a denylisted tool withgate.code: "E_CONTEST_SCOPE"and no longer marks an ordinary tool as gated while a scope is open.licensing.EVAL_ALLOWLISTgainedcad_contest(ten names);cad_license status'sevalToolsreports it.- New env:
CADSOLVER_MCP_CONTEST_ENDPOINT(site base, defaulthttps://featureboard.ai),CADSOLVER_MCP_CONTEST_TIMEOUT_MS(default 20000). Tool count 125 → 126.
(nothing yet)
NativeCAD 0.11.x
v0.11.2 “Jochum”
sha256 nativecad-mcp-0.11.2.mcpb 9cc1bc9a25749af40f9e0355701789099e8a1a490668bc6cf61fee435865e431sha256 nativecad-mcp-0.11.2.tgz 2c789c094662c04edf2519e738c6086fb1a00bf823a2cd36c3bf9621f86a3f92Two crossing cylinders union analytically whichever is named first, and at EQUAL radii
Two cases 0.11.1 left on the general engine, both hit live within minutes of that release:
- The thicker rod named as the tool.
cad_boolean_unionwith the r5 rod asofand the r10 rod astool(the web app'sUnion — f1 + f0) refused the cylinder/cylinder rung withcylcyl_tool_radius_too_large(it reads r_tool < R_base) and fell to the general engine — 500+ PLANE faces and several seconds — while the same two rods the other way round were analytic in 0.11.1. Union is commutative: ops/boolean.js now asks the rung the other way round before giving up on it (_tryCylCylSwapped, gated oncheckLocallike the swapped pierce). The result carriesoperandsSwapped: true. - Equal radii — the Steinmetz cross. Two Ø2 × 20 rods crossed into a plus sign: r = R, which the rung refuses by name because its quartics degenerate there. The general engine answered with 1410 faces (or refused outright at 200 mm). At r = R the intersection is two exact planar ELLIPSES crossing at two pinch points, and NEW
ops/cylCylEqual.jsbuilds exactly that: both walls re-authored with seams through the pinch points, the eight kept wall pieces bounded by the four elliptic arcs. 12 faces, 20 edges, 10 vertices;checkFullclean; volume within 0.02 % of πR²(H+L) − 16R³/3; every edge an exact conic, measured and recorded (worst 2e-16); round-trips through STEP.regime: 'cyl-cyl-equal-analytic', union only.
Both were also the "unperformant" part: every one of these clicks was paying for a general classify-and-stitch boolean whose answer nobody could use.
What a caller might key off:
engineTrailgains the enginescylCylSwappedandcylCylEqual; a refusal by either is carried on the error as.cylCylSwappedRefusal/.cylCylEqualRefusal(codescylcyl_swapped_invalid_result,cyleq_*).cylCyl.equalRadii === trueandcylCyl.pinchVerticeson the equal result;cylCyl.crossingsthere are twokind: 'ellipse'entries.- A subtract at equal radii is NOT taken (it would sever the base into two lumps touching at the pinch points) and answers exactly as before.
v0.11.1 “Järvi”
sha256 nativecad-mcp-0.11.1.mcpb 9a4a9269a599d5a70d106179ea4d8c7a20d059174d0b1ebec7db166aee5564d2sha256 nativecad-mcp-0.11.1.tgz 5168caa599bf27897ed808fd265c27b5cdcf5bed3c179a4c6c8d79ef8a4db202A rod unioned straight THROUGH a rod is analytic — two crossing cylinders no longer come back as triangle soup
Reported by a user (Chris Wilson): cad_cylinder × 2 with perpendicular, intersecting axes → cad_boolean_union → the junction and the thinner rod's wall rendered as dense striations, and cad_fillet_edges had nothing to pick. The cylinder-on-cylinder analytic path (ops/cylCylEdge.js) built a union only as a single BOSS — one end of the tool outside the wall, the other embedded — and refused a tool whose two ends are both outside on opposite sides (cylcyl_boss_root_not_embedded, "a union of a through rod is not the boss this path builds"). The pair then fell through to the general classify-and-stitch engine, which answered a Ø10 rod through a Ø20 × 60 rod with 554 PLANE faces: watertight, −0.14 % on volume, and no cylindrical face left on either rod.
The through rod is now BUILT on the analytic path as two bosses back to back: one Steinmetz quartic per side of the outer wall (the same (Ro, ∓1) crossings the subtract branch already takes), each bounding the tool's wall out to its own cap. On a TUBE the tool wall bridging the bore between the two bore quartics is kept as material boundary too. Measured: 10 faces for the rod pair (6 CYLINDRICAL + 4 PLANE, was 554), 14 for a Ø60 × 3 tube, checkFull clean, volume within 0.014 % of πR²H + πr²L − V_bicyl(R, r) at chord 0.001, every authored edge with a recorded tolerance, and the executor route (cad_cylinder + rotate → cad_boolean_union) returns the same 10-face body. The single boss, the skew pair and a rod that ends inside the far wall answer exactly as before.
What a caller might key off:
engineTrail/regimefor this pair now readscyl-cyl-analytic; the.cylCylRefusalcylcyl_boss_root_not_embeddedno longer appears for a tool whose two ends both clear the outer wall on opposite sides. It still names a tool that ends inside the far wall (its cap disc crosses the cylinder).- The union's result carries
cylCyl.crossingswith TWO entries (side −1 and +1) for a rod, FOUR for a tube. - Filleting the four resulting saddle edges reaches
ops/blend.jswith a real cylindrical pick and refuses BY NAME,blend-sel-non-planar-support— the convex unequal-radius cylinder/cylinder canal band is follow-up work, filed and pinned bytests/test_cadsb-1005.js§F.
v0.11.0 “Hrůša”
sha256 nativecad-mcp-0.11.0.mcpb 38faadd3ec63b327309d99baf73a035b341d34e7de14ae1a53aa9fc8a94ae760sha256 nativecad-mcp-0.11.0.tgz cd330863cc8770f45b858dfba079605ad3b5fb709191a3c5da176a874eca2d00A chamfer whose facet crosses a hole is BUILT against it; a fillet band merely tangent to one no longer refuses
cad_chamfer_edges on an edge whose facet footprint crosses a bore, pocket or island root used to do one of two wrong things on this line: publish ok: true with a self-intersecting body (a Ø6 bore at (10, 4) under a 3 mm chamfer on a 40x20x10 plate), or publish a body whose volume came back negative (the same place as a blind Ø6x4 pocket). It now BUILDS the right answer — the facet is trimmed against the feature's wall (plane ∩ cylinder, an ellipse arc, exact), the wall is re-trimmed with its new top boundary on the facet, and the rails are cut where the feature crosses them. The result is one watertight solid with the hole breaking through the chamfer; method reads selected-trimmed and the result carries footprintTrim naming the crossing edges and the setback that would have cleared them. Volumes measured within 0.008 % of plate − bore − wedge + (wedge ∩ bore counted once) on all five crossed constructions, and the bodies round-trip through STEP unchanged.
What a caller might key off:
- A chamfer that previously came back
ok: trueand self-intersecting now comes backok: trueand clean, with one more face and a different face census. Nothing that built correctly before changed at all — an un-crossed pick still takes the loop rebuild (method: 'selected') byte for byte. - A NEW refusal code,
chamfer-sel-footprint-crossed, is what is left when the trimmed build cannot run: a curved support face, an oblique run-out cap, two crossed picks meeting at a corner, or a cut the boolean declines.err.detail.whysays which,err.measuredFeatureSizeis the largest setback whose footprint clears the feature, anderr.crossingEdgeIdsnames it. It is registered in the operator remedy table and classified in tools/blend_matrix.js under the existingfootprint-crossedkey. cad_fillet_edgesno longer refusesblend-sel-footprint-crossedwhen the band's rail is TANGENT to the feature (it touches at a point and crosses nothing — "exactly at the clearing radius" case). That body builds exactly, 6 PLANEs + 3 CYLINDRICAL_SURFACEs, checkFull clean. A band that STRICTLY crosses the feature still refuses by the same name, unchanged: trimming a fillet band against a bore is still open work (the band ∩ bore edge is a quartic with no closed form when r == R and the axes are offset), and parity B5 therefore stays['REFUSED', 'PASS'].
NativeCAD 0.10.x
v0.10.0 “Honeck”
sha256 nativecad-mcp-0.10.0.mcpb ed779b4be51d0cb7f79d5b248e0ea622189c33e9353b022ce323d7c14c624f67sha256 nativecad-mcp-0.10.0.tgz 550eb3a844ed98e27712642840487e21030cc29a73168293375a198ba2f4fde7Modelling writes build on the kernel worker — the protocol thread no longer parses a build the worker proved (the commit step)
The half an earlier change left open is closed. Every modelling write (cad_box, cad_extrude_profile, the booleans, blends, shells, patterns — everything routed through the document-op surface) is now replayed ONCE, on the kernel worker, and the protocol thread never runs Executor.run for it. Concretely, with two chats or two sub-agents driving one server: a 40-op part being rebuilt in one no longer stalls the other for the length of that rebuild — the other's calls answer while the build runs, because the thread that answers stdio is not the thread building.
- The worker's reply IS the response.
REQ.BUILDwithcommitIndexreturns acommitblock computed where the parse already is: the per-body summary (bbox,volume,watertight— modeling/summary.js's tessellate + checkLocal, now run on the worker), the hole-route advisory, the replay-regression verdict and whether the op landed in the env. runSingleOp reports a proven write from that block. Every refusal keeps its shape and code (E_INVALID_ARGSfrom the validator,E_GEOMETRYfor a failed entry with itsreport,E_TIMEOUTfor a cooperative budget, theE_INTERNALregression refusal) —details.provenBy: "kernel-worker"says which side judged it. - The host's parse is lazy (
src/session/lazy-parse.js). After a worker commit the document'sdocParser/env/consumedByare accessors that replay the COMMITTED ops on this thread the first time a synchronous consumer (session.resolveBody, pmi, quote, review, output, evaluate) reads one — and only the committed prefix, never a candidate op still being proven. A chain of writes never parses on the protocol thread; a write followed by a read parses once. That is the "one parse per document per request". The cost that moved: a read that runs on this thread after a heavy write (a 500-copy pattern, say) now pays that document's host replay at the read rather than at the write — the same replay, later, off the write's critical path. Checkpointed replay applies to both sides now: the worker keeps its own checkpoint ring, so a build there replays the tail, not the journal. - One write at a time per document. A per-document write lock serialises
runSingleOp,cad_create_sketchandcad_build_planon the same document — two writes issued together both land, in order, and neither rolls back the other's candidate. Two documents still build concurrently. - The first op of a document builds on the host. A one-op document is a single primitive the host replays in milliseconds; the worker is spawned in the background by that call so the second op finds it warm. This keeps the first-call latency exactly.
CADSOLVER_MCP_WRITE_WORKERis ON by default, andCADSOLVER_MCP_OP_GUARDfollows it (the pre-run is the build, so the guard no longer costs a second replay).CADSOLVER_MCP_WRITE_WORKER=0restores the pre-1274 host-side build byte-for-byte — the A/B is one switch away. Inline kernel hosts (CADSOLVER_MCP_KERNEL_WORKER=0) are unchanged.- Behaviour a caller may notice: with writes keeping the worker warm, an offloaded read (
cad_mass_properties) is now bounded by the worker's wall clock — under an impossibleopTimeoutMsit is refusedE_TIMEOUTnaming itself, where it used to run unbounded on the host (the gap, now pinned either way). A per-document write is also what a crashing op costs: the thread that died is the worker's, the document rolls back, the session and every other document survive. - Pinned by
tests/test_cadsf-1274-commit.js(30/30): the commit block, zero host builds across a chain of writes, one build at the first read, the acceptance measured (a read on the same document answers in ~1 ms while a 2.5 s union is stalled on the worker), the per-document lock, and the A/B.tests/test_cadsf-1274.jsandtests/test_cadsf-1272.jsupdated for the flipped defaults and the closed non-goal.
Session isolation, batch ergonomics and the agent instructions
Found by the ten-chair text-to-CAD benchmark: two unguided agents sharing one server both built into the ACTIVE document — after a restart, the journal-restored previous part — so exports of "all" carried three parts and every whole-document op paid for all of them.
- New tool
cad_session_reset {confirm:true}. Closes every document and handle in the session, forgets the default document, drops the journal's restorable index and starts a fresh journal file. The epoch does NOT change. The next no-argument modelling call opens a fresh document. Refuses withE_INVALID_ARGSwithoutconfirm:true. cad_batchaccepts host-namespaced tool names (mcp__…__cad_boxiscad_box), and each op in the result now carriesms. A batch is still not a transaction (see batch.js's header) — a failed batch keeps the ops that succeeded.- Instructions gained a "Documents, sessions and multi-part builds" section: no-argument calls land in the active document, start a part with
document:"new"or a reset, multi-part objects are named bodies in one document checked for contact before export, sweep sections are line/arc loops, plan ids are not handles, sweep/loft bodies tessellate at tolerance ≥ 1. Everything a prompt previously had to say.
Every tool call is now timed, always
ToolRegistry.callrecords the last 512 calls —{tool, ms, ok, code, in, out}— into an in-memory ring (src/perf.js). Always on, no flag: the measurement has to exist for the call that was already slow. Measured overhead is well under the ticket's 50 µs/call bound (test/cadsf-1214.test.js).cad_server_infogains aperfsection: p50/p95/max per tool over that ring, plus the ten slowest calls still in it. Purely additive — no existing field moved or changed meaning.- Op-journal
oplines now carryms(the handler's own duration) when the journal is enabled. Additive to the NDJSON format; the reader ignores it, so journals stay readable in both directions. CADSOLVER_PERF=<path>streams one JSONL row per call to that file. Buffered and never awaited by dispatch, withsrc/session/journal.js's write-path rules: one batched append, a 32 MB cap, and a fall back to memory-only after a single flush over 250 ms (the slow-mount case).
NativeCAD 0.9.x
v0.9.0 “Hogwood”
sha256 nativecad-mcp-0.9.0.mcpb 38a6c9621e5e9d46af464bf247f34483119011b2fde53224a3af861bc1da2fccsha256 nativecad-mcp-0.9.0.tgz 07bba19fd024ed24101010168f1053b4827342903ca4d62f5d499f7620d73300Claude Desktop installs of 0.8.0: the kernel worker booted Electron instead of the bundle, and the first call replayed the whole journal
Both shipped early as the unofficial nativecad-mcp-0.8.0-hotfix2.mcpb; this is the release that carries them.
src/kernel-host/worker.jslocated the bundle fromrequire.main.filename. Under Claude Desktop's Electron UtilityProcess that is Electron's own bootstrap script, so every tessellating tool on the installed.mcpb(cad_preview_body,cad_tessellate, exports) died withE_KERNEL_CRASHED— "the kernel worker exited (code 1)" — whilecad_boxand the other non-tessellating calls looked fine. The bundle prelude now records its own path (globalThis.__CADSOLVER_BUNDLE_FILE__) and the worker trusts that first, thenargv[1], thenrequire.main.test/cadsb-825.test.jsloads a real bundle under the Electron-bootstrap condition.- Boot used to replay every document of every past epoch from the op journal before answering the first call — 109 s on a 25-document / 6-boot journal, past Claude's 60 s tool timeout, so the first call after every restart failed. Boot now only indexes the journal; a document is replayed by the first call that references it, and only documents touched in the last two boots are restorable (older ones are listed in the boot notice as trimmed, not replayed). Measured on the same journal:
cad_server_info194 ms,cad_box {document:"new"}68 ms.test/cadsb-828.test.js, 24 assertions.
The modelling write path's pre-run returns the kernel worker's VERDICT, behind CADSOLVER_MCP_WRITE_WORKER (partial)
The op guard replayed every guarded write on the kernel worker purely to learn whether it terminates, then threw the answer away and replayed it again on the protocol thread. With CADSOLVER_MCP_WRITE_WORKER=1 (off by default — the old path is the A/B, byte-for-byte) the pre-run is kernelHost.build (contract.js REQ.BUILD) and the write path acts on the whole verdict: a validator refusal the worker already found is returned as that refusal, not re-discovered; a worker that DIED replaying the candidate becomes a coded, recoverable refusal with the document rolled back and the session intact, instead of re-running the op on the thread that cannot be interrupted; a build the worker proved is reported as proven. The open items each get an answer in code and in tests/test_cadsf-1274.js (34/34): item 2 — cancelling one request no longer refuses its neighbours as "cancelled at the client's request" (bystanders are re-sent and answer); item 3 — a read carries its tool family's ceiling across the boundary and one that outlives it is refused E_TIMEOUT rather than falling back onto the thread it was protecting (transferList is a documented non-goal); item 5 — a query-only session does not pay a second parse for its first offloaded read; item 6 — the worker path and the local fallback return the same value, and a refused document is not re-attempted on every later read. Still on the protocol thread: the commit. A successful write's response needs the live parser here (modeling/summary.js tessellates and runs checkLocal on every published body), so a build the worker proved is still run on the host — that last step is what deletes the host's parse and it is deliberately not smuggled in behind the flag. The ticket stays open for it.
x_t import takes "coarse but located" intersection charts, reads rational vertices homogeneous, and samples fin SP-curves over their own window
- Chordal-error band. The cap on a CHART's declared
chordal_error— max(file tolerance, 1e-3 × the chart's extent) — refused seven previously-green corpus pins (cadsb-548/628/633, cadsf-735/773/880/899): SolidWorks routinely declares 2–15 % on SHORT charts (a 0.6 mm fillet-corner arc at 20 µm, a 2.8 mm thread-crest chart at 0.43 mm), and every one of those parts imported within 1 % before the cap. A declared error above the cap but within 1 × the chart's own extent is now ACCEPTED on the plain path — recorded on the edge as its tolerance (the tolerant topology sizes its bands from it), counted understats.curves.intersectionChartCoarse, and said once as aaccepted coarse chartwarning. Above the extent the worm-gear refusal (XT_UNSUPPORTED_CURVE/XT_CHART_CHORDAL_ERROR_EXCEEDS) is unchanged, and its detail now carriesrefuseAbove. Callers keying off the refusal for the 0.1–100 % band will see an import succeed with a warning instead. - Rational B-splines are homogeneous. An earlier change read a vertex_dim-4 XT vertex as Euclidean
(x, y, z, w). The format stores(x·w, y·w, z·w, w): on the 90545A111 nut's thread flank (w = cos 30°, 60° conic spans) the raw mid-span vertices sit at exactly the end vertices' radius and z/w steps uniformly along the helix — only true of homogeneous storage; on the SolidWorks 2024 thumb screw the fitted flank missed the shared edge's cylinder-fin version by 0.119 mm read Euclidean, and meets it read homogeneous.XtReader._bcurve/_bsurfacedivide by w;_evalNurbsCurve/_deBoorSurfrun de Boor on the stored homogeneous vertices (they used to weight them a second time);XtGraphwrites STEP weights pre-multiplied, so step→x_t→step still round-trips (test_cadsf-808 21/21). Every rational x_t/x_b/.sldprt surface now evaluates differently — the nut moves 213.81 → 229.34 mm³ (re-pinned in test_cadsf-880). - Fin SP-curves honour the TRIMMED_CURVE window and sample adaptively. A tolerant edge's fin SP-curve was evaluated over the whole basis curve in 6 × n_vertices chords: the thumb screw's 18-turn crest helix is one 2-vertex (u,v) line, so each one-turn edge came back as the entire helix in 24 chords and the shank meshed inside out.
_spCurveSplinenow takes the fin's[param_1, param_2]window (ignored when outside the knot range, normalised when reversed) and refines chords whose surface midpoint sits more than 2e-3 mm off, to a 4096-point cap (stats.curves.tolerantSpFinRefined). - Not changed: bare
.x_t/.x_bstill default to mm throughcad_import— the metres default the thumb screw wants (Parasolid's own convention, what every vendor transmit in the corpus uses) breaks round-trips with our own XtWriter, which emits mm. Follow-up filed. - tests/test_cadsb-810.js 49/49; the seven suites above and cadsf-948/963/292, cadsb-692, cadsf-808 green.
x_t units now agree with Parasolid's own convention — exports scale mm→m, bare imports default to m
The follow-up, landed. Parasolid's convention is ONE UNIT = ONE METRE ([S] §4.2.1), evidenced on every vendor transmit in the corpus (Inventor sphere r=0.0508 = 2 in; a SolidWorks 2024 thumb screw 0.006 × 0.019 = a 6 × 19 mm part) — but exporters/XtGraph.js wrote kernel millimetres straight into the transmit unscaled, so a 10 mm box exported to .x_t/.x_b opened as a 10 metre box (1000× too big) in SolidWorks/NX, and importers/ParasolidImport.js kept XtReader's historical mm default for a bare .x_t/.x_b, so a genuine vendor file (its own numbers already in metres) came in 1000× too small — the thumb screw reported as 6 × 19 microns. Could not simply flip the import default alone: doing that broke test_cadsf-735's own-writer round trip, because the writer and reader would then disagree on which side applies the factor.
XtGraphscales every length by 1e-3 (mm → m) by default. Points, radii (CIRCLE/ELLIPSE/CYLINDRICAL_SURFACE/CONICAL_SURFACE/SPHERICAL_SURFACE/TOROIDAL_SURFACE), B-spline/NURBS control points (weights untouched — homogeneousx·w/y·w/z·wterms carry the scaled point, not the weight), and the FACE/EDGE/VERTEX declared tolerances all move through one_len()helper. Directions and normals are never touched (a uniform scale cancels beforeunit()normalises them).opts.units: 'mm'is the escape hatch that restores the earlier byte shape exactly — for a file KNOWN to already break the convention, which today means only our own pre-fix exports. The export report (XtGraph#fromSolid/fromSheet/fromBody's returned.units) carries{ applied, scale }for a caller to log or check.ParasolidImport.fromBufferdefaults a bare.x_t/.x_bto{ units: 'm' }, matchingfromSldprt's existing SolidWorks-embed default.XtReader.build()/XbDecodercalled directly (not through this module) deliberately keep their historical mm default — that call has no way to tell a genuine vendor transmit from one of our own earlier exports sitting on disk in kernel mm, and mm was the reading every direct caller and byte-golden test in this repo was written against.- The two sides move together, so a same-kernel round trip is unit-neutral by construction: default export → default import applies the 1e-3 factor once on the way out and the reciprocal 1e3 on the way back in, netting to the original geometry.
cad_export_body format:"x_t"/"x_b"gained a matchingunitsargument (default"m"). - Every x_t/x_b suite that pairs
XtGraphdirectly with a rawXtReader.build()/XbDecoder.build()call (bypassingParasolidImport, which is most of them — they pin writer/reader MECHANICS, not the vendor unit convention) now passes{ units: 'mm' }explicitly on the write side to stay on the unscaled footing those pins were written against; nothing about their own assertions changed. - tests/test_cadsb-818.js 13/13 (scaled export bytes, a vendor-style bare import at the true size, a legacy unscaled export importing correctly with the
units:'mm'override, a default round trip, and the export report). test_cadsf-735 49/49, test_cadsf-773 24/24, test_cadsb-810 42/42, and every other x_t/x_b suite intests/green.
Turbogenerator demo: the exploded view is a hemisphere, and the engine is bolted down
Two scene changes to web/turbogenerator.html's shipped assets, rebuilt on the host with the placement audit at zero findings and re-exported (scene LOD0–2 + engine file; 171 parts, 941 placements).
- Exploded view.** Every part used to travel straight up, so the plant exploded into one column: a ring of 30 fuel nozzles never left its casing and a gearbox hung under the compressor barrel moved into it. Each placement now travels away from the placement it is mounted to, measured on the two world bboxes' protrusion (which axis the child sticks out past its parent's slab — centre-to-centre cannot tell a nozzle's radial stand-off from its ring station's axial offset),
rests-onspecial-cased to straight up (the 12 m floor plate protrudes 2.75 m past the 250 mm channel it rests on), and the result pushed onto the upper hemisphere: downward component dropped, 0.35 of lift added so a sideways mover clears the pad, normalised. Shipped manifest: 171/171 part directions have a strictly positive up component, 106 leave at an angle, 0 point down;explodeNodescarries a per-nodeunit;scene.explode.hemisphere/upBias; the page uses the node's own direction. - Hold-down.** The four engine feet
rested onthe 8 mm chequer floor plate and their sixteen M24 holes went to nothing. OneHOLDdefinition now drives both sides: the skid's crossmember nearest each mount station moves under the foot (the grid keeps its nine members — two extra members took the enclosure past its 12–18 t literature band) and carries a 420 × 420 × 40 tapped sole plate whose top face meets the floor plate's underside; the feet are drilled from the same hole list the bolts are placed from; 16 M24 hold-down bolts (grip summed from the clamped stack) + 16 washers go through the floor into the sole plates. Load path engine → mount → floor plate → sole plate → crossmember → skid channels → pad → ground, every hop a declared, audited joint. The forward feet's inboard holes sit 230 mm past the leg face (at 200 the Ø44 washer straddled the leg by 4 mm — the audit caught it).audit_scene.jsgains a fastener census (r.fasteners): everybolts-to/flange-tojoint asked whether realhw-*hardware is declared on it — reported, not a finding, so it can be ratcheted down (12 of 622 today). Fastener recipes (hexPts,HEXISO 4017/4032/7089 proportions for M6/M10/M16/M24 with clearance holes,hexBolt) are shared at module scope.
Tests: test_cadsb-788 78/78 (hemiUnit / escapeUnit / meanUnit fixtures, every part and node direction on the hemisphere, > 20 parts leave at an angle), test_cadsf-1259 4732/4732 (feet bolted, 16/16/4, load path, each bolt clamps its 25 mm plate through the floor into its sole plate), test_cadsf-1251 3660/3660 (enclosure 30 parts / 17.7 t, engine 43), test_cadsf-1253 212/212, test_cadsf-1252 731/731.
The selective chamfer runs out at a REFLEX in-face corner instead of refusing
chamferSelectedEdges' 1-of-3 run-out measured where the chamfer rail lands on the side edge as an unsigned distance and refused when the landing fell "PAST the vertex" — every one of the 193 chamfer-sel-setback-geometry-mismatch rows of the 2026-09-02 sweep. The landing parameter is |offset| / sin(phi) with phi the face's interior angle at the vertex, so it is negative exactly at a reflex corner: every vertex of a polygonal hole or pocket, read from the plate's cap face. The parameter is now signed; a reflex landing lengthens the side edge onto the landing instead of shortening it, and the in-cap wall triangle (V, landing on f, landing on the other support) is absorbed by the cap face through the existing capExtras splice, so the vertex dissolves and no extra face is emitted. Bodies are watertight (checkFull ok, mesh census clean) and exact in volume: d²L/2 for a perpendicular cap (square hole, d=1, L=12 → 6.000 mm³), d²L/2 + cot(phi)·d³/3 for an oblique one (hex hole, d=1, L=8 → 3.807550 mm³); the result reports runoutWalls. Still refused: a setback longer than the side edge (same chamfer-sel-setback-too-large code, now worded "would overhang") and, defensively, a run-out landing past the vertex on BOTH side edges. Convex run-outs are byte-identical. Consequence: the partitioned blanket chamfer now bevels the polygonal-hole rim edges it used to skip (hex-hole plate 12 → 18 strips; pocket plate 12 → 14).
Periodic faces whose period is "closed by an inner bound" split instead of refusing curved_seam_wrap
Each bound of a periodic face (cylinder, sphere, torus) was branch-continued only within itself, so a hole whose first sample inverted onto the far side of the atan2 cut sat a whole period away from the outer bound that contains it; the chart then read as spanning more than a period and the face was refused curved_seam_wrap ("… its OUTER bound spans only X of 2pi: the period is closed by an inner bound") — 11 of the 26 rows of the 2026-09-06 planar/planar battery. reconcileBranches now shifts every loop by whole periods onto the FACE_OUTER_BOUND loop's branch (the largest-chart-area loop when none is flagged) before the domain is measured; a whole-period shift renames the branch and moves no point, and k = 0 leaves a loop value-for-value untouched. Separately, the artificial seam was cut at the rail's first sample regardless of holes, refusing a wrapping face for the accident of where its rail started (6 of 26); chooseSeamOrigin keeps that position when both cuts miss every hole, else takes the first rail sample where neither cut lands in a hole, else a free-gap midpoint, and keeps the default — and the existing refusal — when no position is free (a hole wider than half the period). The straddle refusal now names the cut the hole actually crosses. Synthetic tube-with-window bodies that refused on both mechanisms now build valid watertight bodies at 0.16% / 0.013% of analytic volume; 11 neighbouring boolean/faceSplit suites are byte-identical.
Tessellation repair closes the last open rim quads with a Steiner centroid fan
_repairWatertight's hole filler ear-clips each open rim, and an ear may not reuse a diagonal the surviving skin already covers twice (a third cover is a non-manifold edge). A four-vertex rim has exactly two diagonals; when both are saturated every ear is refused, the loop is re-walked and re-refused on every later pass, and the mesh stays open — after an earlier release removed the figure-eight walks this was the ONLY shape left open on the 38-file robustness subset (4c6573f095dd_ROB-12309 4 open, 67010a9d36af_ROB-12537 4, 1fb99b00c6ae CP_Radial 7). When ear clipping leaves a simple rim wholly unfilled and every rim edge sits at exactly one use, the repair now mints ONE vertex at the rim centroid and emits one triangle per rim edge: no diagonal is consumed, so saturation cannot block it, and the census effect is exact (every rim edge 1 → 2 uses, every spoke used by its two fan triangles). A/B over the 38 files: open edges 15 → 0, non-manifold 0 → 0, duplicate windings 32 → 12, closed shells 35/38 → 38/38. meta.repair.steinerFans counts the fans, meta.repair.steinerFanWindingCost the irreducible directed-edge conflicts a fan on an already-conflicted rim costs (67010a9d36af: 2, buying 4 open edges). Guarded to a genuine last resort (simple cycle, rim edges at one use, no fan triangle under AREA_EPS, at most FAN_MAX rim edges); CADSOLVER_NO_STEINER_FAN=1 restores the old behaviour. Meshes that already repaired clean are byte-identical.
A blanket fillet no longer returns a self-intersecting body when two bands meet on one face
Found under an earlier change: a 10x10x10 plate with a 9x9 hole (a 0.5 mm frame) asked for cad_fillet_edges r=1 came back ok, 16 strips built, gate {openEdges: 0} — and cad_dfm_check reported error-severity self_intersection. The wall gate has since closed that frame; what it leaves by design is two bands each fitting its own face and built THROUGH EACH OTHER — the top and bottom fillets of a 10x10 plate 1.5 (or 1.9) mm thick at r=1, the outer and inner rims of a 1.5 mm frame wall. Three changes:
- The dry run is asked before a blanket or multi-edge build. The pre-build probe used to decline any selection whose edges share a vertex ("two picked edges share a vertex (corner case)"), so the whole-body path and every corner pick were built blind. It now analyses them, skipping only the corner-station tests at a vertex two picked edges share (the corner construction's own business) and the saddle-vertex ends of a multi-edge pick; single-edge picks are byte-identical.
- Band collision is measured pre-build, with a number. Two picked edges bounding one support face, not sharing a vertex, whose footprint strips overlap are refused before anything is authored: the blanket path SKIPS the pair with the existing
partition-radius-exceeds-face(the skip record carriescollidesWithandrMax, the largest radius at which the two strips clear — 0.75 on the 1.5 mm plate) and dresses the rest; the selective path REFUSESblend-sel-radius-exceeds-facewith the same detail andmeasuredFeatureSize. The wording says the OTHER band is in the way, not the far side of the face. An edge already reported by the per-edge wall measurement keeps that finding untouched. - The partition gate sees self-intersection. The whole-body validity gate counted open edges only; a closed shell whose faces cross passed it. It now also runs the face-face scan (as a delta against the input body, so an imported body that already self-intersects is not refused over it), and a self-intersection that survives the chain bisection is REFUSED (
blend-result-not-watertight, the document rolled back) rather than returned.result.gategainsselfIntersections/selfIntersectionsInput;openEdgesis unchanged for existing readers.
Behaviour that moved besides the ticket's cases: a body whose every convex edge collides with a neighbour and whose remaining edges are too short (a 3x3x10 bar at r=1.6) now refuses blend-edge-too-short instead of returning a 3-edge partial; the chamfer-then-fillet composition (C3 then r=2 on a 30x30x12 box) now refuses blend-convex-candidates-failed with a per-edge report (the truncation facets' 60° corners cannot carry an r=2 rail) instead of dressing 4 of 48 edges for 0.7 mm³. A dedicated partition-gate-self-intersecting code needs the remedy table regenerated (tools/gen_refusal_remedies.js) and is left for the next pass.
glTF export: 4-byte-aligned quantized vertex attributes are now the default, and the poster renderer honours bufferView.byteStride
An earlier change added GltfExporter's options.alignVertexAttributes (pads each KHR_mesh_quantization SHORT/BYTE VEC3 vertex element — 6 or 3 bytes — up to a 4-byte multiple, per glTF 2.0 §3.6.2.4) but left it off by default, so every quantized export the pipeline had shipped was technically invalid: MEASURED at 204 MESH_PRIMITIVE_ACCESSOR_UNALIGNED gltf-validator errors per LOD file on the turbogenerator scene, 0 with the flag on. It stayed off because (a) tests/test_cadsf-752.js pinned quantization's own size win against the unpadded baseline (> 25 %), and (b) tools/render_glb_shots.js — the zero-dependency poster renderer, an independent GLB reader — de-interleaved vertex accessors WITHOUT reading bufferView.byteStride, so a padded (strided) file decoded to the wrong world bounds there.
Both blockers are fixed, and alignVertexAttributes now defaults to true. false remains a supported explicit opt-out that reproduces the exact earlier byte layout.
packages/interop/src/GltfExporter.js:alignAttributesis nowoptions.alignVertexAttributes !== false(was!!options.alignVertexAttributes). An UNQUANTIZED (FLOAT) export is unaffected either way — every FLOAT VEC3/VEC4 element is already a multiple of 4 bytes, so there is nothing to pad — and stays byte-identical to before this ticket.tools/render_glb_shots.js'saccessorArray()now readsbufferView.byteStrideand de-interleaves element-by-element viaDataViewwhen the stride is wider than the tightly-packed element size, the same ruleweb/src/glb-read.jsalready followed. The fast contiguousnew TypedArray(buffer, offset, length)path is kept for the (still common) case where the view has no stride or is already packed at the element size.tests/test_cadsf-752.js's size-cut assertion is rebased: it now measures BOTH the unpadded opt-out (alignVertexAttributes: false, pinning the original> 25 %invariant against the baseline it always meant) and the new aligned default (measured ~23.6 % ondrilled_bracket.glb, asserted> 20 %), plus that the aligned file is strictly larger than the unpadded one.tests/test_cadsf-745.js'scheckAccessorBoundsno longer requires a vertex bufferView to be tightly packed at the raw element size — a 4-byte-alignedbyteStrideis now a legal, and expected, layout; it additionally asserts any declaredbyteStrideis itself a multiple of 4.demos/turbogenerator/export_scene.jsalready passedalignVertexAttributes: trueexplicitly (added alongside landing) — the shippedweb/turbogenerator/scene-lod{0,1,2}.glbwere already validator-clean before this ticket; this ticket makes that the behavior for every OTHER caller that was relying on the (unsafe) default.- New
tests/test_cadsf-1256.js: a quantized export has everybufferView.byteStride % 4 === 0and every accessor's total byte offset aligned to itscomponentTypesize; a FLOAT export is byte-identical regardless of the flag;render_glb_shots.js's reader decodes an aligned quantized file to the same positions as the unaligned one; the explicitalignVertexAttributes: falseopt-out still produces the old, unpadded layout.
SolidWorks multi-body Parasolid exports open: ASSEMBLY / INSTANCE / TRANSFORM roots are read
SolidWorks writes every multi-body part it exports to .x_t as an ASSEMBLY at index 1 with one INSTANCE per body, each carrying a TRANSFORM — and cad_import refused the first record by name (XT_UNSUPPORTED_NODE: node ASSEMBLY (10)). The three layouts are now read at base schema 13006 (every embedded-schema SolidWorks transmit), evidenced on two genuine files — a 2024 McMaster-Carr 92545A199 thumb screw (two bodies) and the 2010 Sparkfun display assembly (eighteen) — and every BODY under the root is grafted, in stream order, exactly as a partition transmit's bodies are. The instance graph is reported on the import's header.assembly (one entry per instance: part, transform, identity); a non-identity transform is a warning naming the instance, never a silently moved body, because only identity records are evidenced. The same files pinned the text layouts for INTERSECTION / CHART / LIMIT / TRIMMED_CURVE / SP_CURVE / GEOMETRIC_OWNER / SURFACE_DATA / BLENDED_EDGE / BLEND_BOUND / SWEPT_SURF / HELIX_*_FORM (the binary decoder's, now in text), the ATTRIB_DEF grouping edit scripts address (six fixed fields), and definition records for node types absent from base 13006 (INTERSECTION_DATA). Rode along: XtReader.build() ignored opts.bodyIndex, so every multi-body partition had imported as N copies of its first body — the thumb screw now imports as its 8-face shank AND its 1404-face knurled head.
A hung kernel op is cancelled and refused E_TIMEOUT by name instead of taking the session
An earlier change bounded every op that POLLS a deadline; a non-cooperative loop with nothing to poll (blend/loft/sweep/tessellation — original symptom) still ran until the client gave up and the host app restarted the server, which reset sessionEpoch and dropped every body.
- The kernel host now enforces a HARD ceiling per request.
tessellate,queryandsyncaccept{timeoutMs | deadlineAt, opName}; a request that outlives it terminates the worker thread (mechanism, on a timer) and is refusedE_TIMEOUTwithdetails{op, reasonCode: "op-time-budget-exceeded", budgetMs, elapsedMs, timedOut: true, sessionSurvives: true, serverRestarted: false}— the same code and reason code a cooperative budget refusal already uses, so a caller never has to branch on which clock noticed. The session, its epoch and every body survive: the host owns the op journal and replays it into the next worker.cad_server_info-style host stats gaintimeouts. Off unless a ceiling is given (orCADSOLVER_MCP_KERNEL_OP_TIMEOUT_MSis set) — no existing call gains a deadline it did not have. The clock starts when the worker announces itself, not when the request is queued: the worker is spawned lazily and its first act is to require the whole kernel (seconds on an idle box, ~49 s measured on a loaded 2-vCPU one), and charging that to the op would refuse healthy calls — hardest on the first call after any death, i.e. exactly the call a client is retrying. - Opt-in guard for the modelling build (
CADSOLVER_MCP_OP_GUARD=1). With it on, a modelling call pre-runs its document on the worker under the call's ownopTimeoutMsbefore the protocol thread commits to a build it cannot interrupt; a document that does not finish is refused with the existing E_TIMEOUT budget refusal naming the op,documentRolledBack: true. Off by default because it costs a second replay (the double parse only closes whenExecutor.runitself crosses the boundary), and because pricing the ceiling on the whole rebuild rather than on the new op is the open question. - Test knob
CADSOLVER_TEST_SLOW_OP(+CADSOLVER_TEST_SLOW_OP_MS) makes a named op stall inside the worker's replay, in a loop with nothing to poll. Unset in every normal boot.
cad_fit_surface: degenerate clouds now REFUSE, refusals carry a reasonCode, and a fit can be trimmed
Three review follow-through changes to one tool. The first is a behaviour change a caller can key off:
- A degenerate point cloud is refused instead of fitted.
cad_fit_surfacewithpointsthat are all coincident, or all on one line, used to answerok: truewith a meaningless near-zero-extent sheet — the thin-plate smoothing escalation always found a solvable system. Such a cloud now refuses withfit_surface_degenerate_cloud(no extent) orfit_surface_rank_deficient_cloud(rank 1: it determines a curve, so fit it withcad_fit_curve). The gate is a measured extent RATIO — a cloud whose span across its own principal axis is under 1e-6 of the span along it — so no cloud that carried real two-parameter information before is affected. - Every
cad_fit_surfacerefusal now carries areasonCode, the waycad_mesh_to_brep's already did: ninefit_surface_*codes from the fits themselves plus the sevenfreeform_sheet_*codes from publishing the patch as a sheet. All sixteen have a row in the operator remedy table, so a refused op comes back withremedyattached. - New optional
trim(plustrimSamples,trimTol). Loops in the fitted surface's own (u,v) square — first the outer boundary, the rest holes — bound the published face instead of the patch's four natural sides, which is what finally givesNurbsSurface.trim()'sTrimmedSurfacea consumer. Each (u,v) side is sampled on the surface and authored as the B-spline through those samples; the deviation from the true surface curve is measured and reported onanalytic.trim.deviation, andtrimTolis the value above which it is refused (freeform_sheet_trim_deviation) rather than delivered. Command-schema 1.20.0 → 1.21.0 (three added OPTIONAL fields, one new field kindtrimLoops); acad_fit_surfacecall withouttrimis unchanged.
cad_thicken: a planar sheet with a curved rim is no longer refused
thicken_edge_not_straight is retired. A single-face sheet whose surface is a PLANE but whose boundary carries a CIRCLE or a B-spline — a disc, a rounded-corner blank, an arc-profiled part's flat cap — used to refuse with
> thicken: edge #… is CIRCLE — v1 thickens straight-edged sheets only
and now thickens, taking the same analytic offset path a curved sheet takes (report.path === "analytic"). The offset of a plane is the parallel plane exactly, so nothing about the answer is approximate except the rim curves, which are re-solved and recognised — a LINE stays a LINE, a CIRCLE stays a CIRCLE — with the deviation reported in report.deviation. A flat sheet with a polygon rim still takes the v1 translation path and still reports an EXACT volume; report.path is absent there, which is how a caller tells the two apart.
Refusals that remain, and now cover everything that used to hide behind the planarity codes: thicken_surface_unsupported (a surface whose constant-distance offset this kernel can neither state exactly nor measure, e.g. SURFACE_OF_REVOLUTION), thicken_offset_refused (the offset surface itself collapses — relayed with the offset's own code on .causeCode), thicken_face_has_holes, thicken_face_degenerate. Both thicken_face_not_planar and thicken_edge_not_straight stay published in REFUSAL_CODES so a caller switching on them still compiles; nothing raises either for a curved sheet any more.
cad_chamfer_edges: a corner where MORE THAN THREE picked edges meet now builds
chamferSelectedEdges refused every vertex past a trihedral one — chamfer-sel-vertex-too-many-selected when all k picks met there, chamfer-sel-vertex-valence otherwise — so a pyramid apex, an n-gonal boss top or an octahedron vertex could not be chamfered by selection even though the whole-body path (chamferConvexEdges) has beveled them since day one. A selection that names every edge at a valence-k vertex (k ≥ 3) now builds the planar k-gon truncation facet there.
What a caller keys off:
- Fewer refusals, no new success shapes. Nothing that built before builds differently: the trihedral corner still goes through the exact 3×3 setback solve it always did, byte for byte.
chamfer-sel-vertex-too-many-selectedis effectively retired. It now fires only for an impossible count (more picks at a vertex than it has edges). A k-of-k pick that used to raise it builds.chamfer-sel-vertex-valencenarrows to the PARTIAL pick. Past a trihedral corner the facet is bounded by all k end segments, so leaving one edge sharp leaves it open; the message now names every edge at the vertex so the fix is in the refusal.- A new coded refusal case, on an existing code. A valence-k corner only admits a setback point when its k support planes are tangent to one cone of revolution about the corner axis (
ops/blendVertex.js). A corner that is not — a skewed apex — refuses aschamfer-sel-corner-inconsistent, with the measured miss in the message and the note that the condition is homogeneous in the setback, so a smallerdistancewill not rescue it.
cad_chamfer_edges: a mitred corner no longer needs square dihedrals
A selective chamfer (edges: [...]) where two picked edges share a corner with a third, un-chamfered one used to refuse with chamfer-sel-corner-inconsistent (or chamfer-sel-miter-geometry-mismatch) unless BOTH support faces stood at 90 degrees to the face the two picked edges share. The mitre points were placed "one setback inside each outer face", which is where the chamfer plane actually falls only in that square case. They are now the chamfer planes' own intersections, so a tetrahedral corner, a hex boss and a drafted wall mitre exactly; where the two dihedrals DIFFER the two chamfer planes cross the third edge at two points and the seam picks up one extra segment (a kink on the shallower edge's outer face) instead of being refused. A 90-degree corner is unchanged, and no refusal code was added or retired.
NativeCAD 0.8.x
v0.8.0 “Fricsay”
sha256 nativecad-mcp-0.8.0.mcpb 7fedd38874da10e4ca51ba3bf814970375e660c62bdff7014189760d05d90bf2sha256 nativecad-mcp-0.8.0.tgz 6b272c48483ac270f99d59feffe2f41b78365cd9846a57ac8b7d0084fb7ec2b4glTF: B-rep edges are a pipeline primitive, primitive extras survive, and a LINES primitive no longer refuses
Three additive changes in packages/interop. Nothing existing changes shape; an export that asks for none of them is byte-for-byte what it was.
GltfScene.edgesPrimitive(edges, opts)is new. It emits the B-rep edge convention —mode: 1(LINES), POSITION only, taggedextras: {kind: "edges"}— whichweb/src/glb-view.jsalready honours as the edge overlay instead of inferring one from triangle angles.GltfScene.fromMeshesnow takesbody.edgesandGltfAssembly.fromAssemblytakescomponent.edges, both putting the edge set on the same glTF mesh as the triangles, so an instanced part places its shaded surface and its edges through one node.edgesis part of the assembly's mesh identity: an edged and an un-edged component sharing ameshKeyare two meshes, not one.GltfExporterpasses primitive-levelextrasthrough. Onlymesh.extrassurvived before, and the edge tag lives on the primitive — a mesh carrying both its triangles and its edges cannot saykind: "edges"at the mesh level without mislabelling the triangles.GltfExporter's index-count divisor followsprimitive.mode. It was hard-coded to 3, so every LINES (and POINTS) primitive was refused withindex count N is not a positive multiple of 3— an error about triangles that was true of nothing in the file. LINES now divide by 2, POINTS by 1.GltfExportergainedoptions.alignVertexAttributes(defaultfalse). glTF §3.6.2.4 requires each vertex-attribute element to be 4-byte aligned inside its bufferView. AKHR_mesh_quantizationSHORTVEC3is 6 bytes and is not: measured, each LOD file of the turbogenerator scene reported 204MESH_PRIMITIVE_ACCESSOR_UNALIGNEDerrors from the Khronos validator, and zero with the flag on. It is opt-in because switching it on everywhere costs 2 bytes per quantized VEC3 vertex and becausetools/render_glb_shots.jsreads accessors without honouringbufferView.byteStride;web/src/glb-read.jsdoes honour it. Making it the default and fixing that reader wants a ticket of its own.
A plane × B-spline blend LOOP is no longer declined for its size, and a partial one is a result
A call that used to refuse now succeeds. An earlier change declined a reentrant plane × B-spline loop whose edges × faces product was over a rebuild budget (4 000) — the turbine blade's ~58-edge airfoil root on a 387-face body came to 22 446 — re-throwing the original E_GEOMETRY / NON_PLANAR_SUPPORT with the estimate on .planeSplineChain. Every band re-authored the whole solid; a band now re-authors three faces (its two supports and itself) whatever the body's face count, and everything else survives by identity. The budget is retired.
What changed for a caller:
report.faceRebuildsis new:{ perPickMax, total, bodyFaces }, measured.report.partial,report.refusedandreport.refusedByCodeare new. A loop where some edges cannot take the ball now blends the rest and says so instead of the pick standing or falling as a whole — a root fillet with a too-tight trailing edge left sharp is reported aspartial: truewith that edge's ownBLEND_PS_BALL_EXCEEDS_CURVATURE(or other) code inreport.edgeReport. A caller that readreport.blended === report.selectedas "the fillet is complete" still reads correctly; one that assumed success meant every picked edge was blended should readpartial.- a loop now stops instead of running past your timeout.
report.timeBudget{ ms, budgetMs, exhausted }is new: the chain spends a 20 s wall-clock budget on the picks (opts.planeSplineChainMs,Infinitylifts it) and reports every edge it did not get to asstatus: "skipped"withcode: "BLEND_PS_CHAIN_TIME_BUDGET", returning the bands it built. Measured: the turbine blade's 177-edge root loop is 103–111 s of geometry, which used to mean the call died and the caller got nothing; one call now returns in 49 s with 17 bands built, 32 edges refused by code and 128 named as not attempted. Pick the remaining edges in a second call — ids follow across the rebuild. report.auditedPerPick/report.auditRetriedsay whether the per-pick watertight audit had to be paid (the chain defers it and pays one audit on the body it returns; if that body is defective the whole loop is re-run audited, so nothing is published that the per-pick audit would have refused).- the size cap that remains is an absolute 512-edge cap (
opts.planeSplineChainBudget, now read as an edge count). Over it the original classified refusal is re-thrown byte for byte, with.planeSplineChain.code = BLEND_PS_CHAIN_TOO_MANY_EDGES— thefaceRebuilds/budgetfields that field used to carry are gone. - a ball that runs off a narrow support in the SPINE_OPEN diagnostic used to throw an uncoded
ReferenceError: clip is not definedinstead ofBLEND_PS_SPINE_OPENwith the measured face width. Fixed; the coded refusal and its wording are what a caller now sees.
Appending an op no longer replays the whole document
Nothing about a call's ANSWER changes; what changes is what it costs. Every modelling call used to re-execute the document's entire op list, so the price of any op grew with the document's history — a trivial cad_box measured 44 ms in an empty document and 663 / 1369 / 2524 ms as three general-engine booleans landed in it (root cause; itself only bounded the batch clock so this could not take the session down with it). The server now retains the kernel's per-op checkpoint (commands/rebuild.js) after each successful op and replays only the appended one: the same four measurements are 6 / 32 / 43 / 84 ms.
The appended op's result is byte-identical to a full replay — same handles, same entity ids, same provenance, same reports, same refusals — and packages/mcp/test/cadsf-1152.test.js asserts exactly that by running each scenario twice, once with the checkpoint and once without, and comparing the transcripts whole. A checkpoint is dropped rather than trusted whenever it cannot be proven equivalent: an edited or removed earlier op, a cad_param_set rebuild, a change in the executor options the document replays under, a PMI-bearing document. A refused op still leaves no residue and no checkpoint. CADSOLVER_INCREMENTAL_REPLAY=0 restores full replay.
cad_fillet_edges now builds the REENTRANT plane × B-spline root fillet
A call that used to refuse now succeeds. A fillet on a reentrant (concave) edge between a PLANE face and a B_SPLINE_SURFACE_WITH_KNOTS — the root of any boss, rib or blade sitting on a freeform face — refused E_GEOMETRY / NON_PLANAR_SUPPORT, "the rolling-ball blend is only defined between PLANAR faces" (the underlying kernel code was BLEND_PS_EDGE_NOT_CONVEX, since ops/blendPlaneSpline.js was convex-only). It now builds: the ball rolls on the OUTSIDE of both supports and the band is emitted as an exact b_spline_surface_with_knots that adds material in the corner, G1 to both supports (measured, refused if not met), watertight, and a clean STEP round trip. A whole intersection LOOP is applied edge by edge and reported per edge in report.edgeReport; report.method is plane-spline-rolling-ball for one edge and plane-spline-rolling-ball-chain for a loop, and each row carries side: "concave".
New error codes a caller may now see on such a pick, both carrying the measurement they name:
BLEND_PS_BALL_EXCEEDS_CURVATURE— the radius is at or past the B-spline support's minimum radius of curvature on the side the ball rolls on (minCurvatureRadius,curvature,at: {u, v, point},face,edge). The turbine blade's trailing edge is the real instance.BLEND_PS_RADIUS_OVERRUNS_SUPPORT— the planar support has less than the radius left beyond the picked edge (width,face,edge).
Changed meaning: BLEND_PS_EDGE_NOT_CONVEX no longer means "reentrant". It now fires only for a TANGENT edge, or one whose convexity cannot be read — both senses of a real corner are built.
A loop costs one full-body rebuild per edge (each band re-authors the whole solid — ~0.6 s per edge on a 23-face body). demos/turbine_blade's ~58-edge root loop on a 387-face body therefore reaches the construction but runs past the tool call's own request timeout; it is no longer refused, and it does not yet complete in one call. tests/test_cadsf-1199.js's "root fillet refused" pin is therefore NOT flipped by this ticket.
Unchanged: convex plane × B-spline picks, the general ops/blendNurbs.js ball, and every analytic (plane × plane, plane × cylinder) blend are byte-for-byte what they were; a pick that refused before and still cannot be built refuses with the same code it did. Not fixed: the volume over-move on the fitted band — measured at 1.78× the rolling-ball bound on a 90° corner, identically for the convex and the reentrant sense of the same corner (see docs/BLEND_EVALUATION.md §5.4).
Turbine-blade showcase — a four-wall public page
No tool behaviour changes. demos/turbine_blade/build_page.js now carries an omit set, OMIT_DEFAULT = ['structural']: the generated web/turbine-blade.html is a four-wall page — castability, rim load, tip clearance, aero. The structural study is internal, stays in demos/turbine_blade/results/box.json in the private repo, and appears nowhere on the site: there is no card, no placeholder, no marker, no label, no manifest key, and no sentence that hints a fifth measurement exists. The constraint box's neck-width panel shows the rim-load points and its note only; the nominal fails one wall (castability) and the picked point (tTE 1.6, neck 9, h 60) passes all four. A build-time guard throws if the HTML or the manifest contains structural, withheld, private study, partner, meshfree, basis order, 8000, cell resolution, safety factor, an SF <digit> figure, von Mises, fea_, or MPa outside the rim-load card (whose hoop stresses are our own ops/discStress.js closed-form arithmetic and must be labelled as such); the FEA vendor's name is refused unconditionally. --include-structural lifts the omission for the gated/internal build only and must never be used for the public site. The page also gains a view control bar — five display modes (shaded, shaded + edges, wireframe, x-ray, drawing/hidden-line), four material presets (graphite, satin, nickel, ivory), four standard views, a ground toggle and a reset, remembered in one localStorage key and re-applied after every point swap — and now defaults to graphite at exposure 0.82 instead of the near-white fallback that ACES clipped to a flat block. The blade still rendered white after that, for a reason the preset could not fix — three r128 predates colour management, so an authored sRGB hex was used as a linear albedo (0x5f6670 at ~0.37 instead of ~0.11) and graphite / satin / ivory came out ten per cent apart — so glb-view.js now decodes the fallback colour out of sRGB, takes an opts.materialOverride that applies the picked material to every primitive, and the GLBs are built from each point's blade.step through the kernel rather than its blade.stl: SurfaceTessellator at a 0.05 mm chord tolerance with a 35° crease gate gives the surfaces' own normals, and its extractStepEdges() ships every B-rep edge, sampled off its own curve, as a mode 1 LINES primitive tagged {kind:"edges"} that the viewer uses as the edge overlay in place of an EdgesGeometry guessed from triangle angles. It is driven by an additive web/src/glb-view.js handle API — setDisplayMode(mode), setMaterial(preset | {color, roughness, metalness}), setView(name), setGround(on), setExposure(x) plus displayMode / material / view / ground getters and the exported DISPLAY_MODES, MATERIAL_PRESETS and VIEW_DIRS — with frameCamera() taking an optional direction. Every setter is a no-op before a scene loads (the request is queued and applied on load), and a page that never calls one renders exactly as it did.
Licensing + evaluation loop
Three changes in this release alter what an existing caller gets back from a call that used to succeed. They are listed first, in the order a caller meets them.
BREAKING — an unlicensed server now refuses every tool outside the evaluation loop. Until 0.7.0 a missing licence key cost a watermark and nothing else ("nothing is disabled either way" — that sentence is gone from cad_server_info, licensing.js and the docs, because it is no longer true). An unlicensed process now answers E_LICENSE_REQUIRED — recoverable: true, with details.registerUrl, details.tier: "unlicensed", details.evalHint and details.allowedWithoutKey — for every tool outside licensing.EVAL_ALLOWLIST (cad_server_info, cad_help, cad_license, cad_evaluate, cad_eval_report, cad_eval_packet, cad_eval_share, cad_report_bug). The refusal is raised in ToolRegistry._dispatch, BEFORE argument validation, so junk arguments to a gated tool return the licence code rather than leaking E_INVALID_ARGS; it is returned on every call, not once per session; and an unknown tool is still E_UNKNOWN_TOOL. cad_server_info gains a gate block (enforced, tier, errorCode, registerUrl, statement) and cad_help marks a gated tool as gated. Registration is FREE: the new free tier is a real CSLV1 key issued after e-mail confirmation, opens the full toolset on Education / Maker terms, and is the one licensed tier that still watermarks — the gate is about registration, not payment. cad_license activate now PERSISTS the key (~/.nativecad/license.json, override CADSOLVER_LICENSE_FILE, written 0600); deactivate deletes it. Offline validation as before — no activation server exists to be down.
BREAKING — cad_evaluate requires recorded terms when unlicensed. An unlicensed caller must pass criteria[] ({id, statement, mustPass, category?}) and consent {sendReport: true, contact}; without both the call is refused with E_EVAL_CONSENT_REQUIRED, carrying the consent statement verbatim, its sha256 and the 30-day approval duration — nothing is evaluated and nothing is written. A LICENSED caller's existing shape is unchanged. When criteria are supplied they are scored pass / fail / not-assessable (an unroutable must-pass criterion makes the verdict incomplete, never pass), and the LICENSE §2 approval is filed beside the run as <runId>.approval.json. The contact identity lives in that file and nowhere else: the run document — the thing the packet and the share are built from — carries a consent hash and a statement digest instead.
BREAKING — cad_eval_packet defaults to disclosure L1: no customer geometry. The packet used to ship cropped snippets of the customer's model by default. The ladder is now L0 (descriptions only), L1 (the default: a synthetic reproducer rebuilt from primitives, verified to reproduce the same failure mode and error code before it is included) and L2 (cropped customer geometry, opt-in only via disclosure: "L2" or a per-issue byIssue table). The manifest states a level for EVERY selected issue, with the level asked for beside the level shipped and a fallbackReason when they differ. Anything downstream that assumed a snippet by default must now ask for L2 explicitly.
cad_eval_share posts by default. A confirmed share now POSTs one JSON document (schema 2) to <CADSOLVER_MCP_VENDOR_BASE_URL>/api/ eval-intake — default https://featureboard.ai/api/eval-intake — carrying the archive base64-encoded, the consent block (the only identity in the document), the criteria, the approval record and the scorecard's gap-close rundown as plain text. It was previously local-only. Set CADSOLVER_MCP_SHARE_OFFLINE=1 to force the ready-to-send local bundle and open no socket (the air-gapped image sets it), or point CADSOLVER_MCP_SHARE_ENDPOINT elsewhere. An unreachable endpoint always falls back to the bundle — a failed send never loses the packet. The preview stage is unchanged and still touches no network.
New: cad_gap_registry. The register of what this kernel cannot do — each gap with its {opClass, failureMode, geometryPattern} signature, status, ticket, day estimate and evidence. action: "list" reads it; action: "match" classifies a run's issues against it and returns the gap each one evidences, ranked, with unexplained issues coming back as unclassified:<opClass>:<failureMode> stubs. cad_eval_report uses the same classifier to cost what stands behind a failed criterion ("to meet your criteria we need to close N gaps, ~X weeks" — always a floor, and it says so), scores the caller's criteria, and renders an agent-authored gapCloseNarrative in a labelled block. The report still names no file, no path and no geometry. cad_gap_registry is on the eval allowlist — the register holds only our own gaps, so an unlicensed session can classify before any key exists.
Verification. packages/mcp/test/cadsf-1229-e2e.test.js walks the whole journey through the real protocol boundary in one process: unlicensed boot → E_LICENSE_REQUIRED → E_EVAL_CONSENT_REQUIRED → an evaluation on recorded terms → the report → an L1 packet → an offline share bundle → a free key → the same call succeeding, watermark on → a studio key, watermark off → deactivate, gated again.
The turbine-blade design loop, in public. web/turbine-blade.html is generated by demos/turbine_blade/build_page.js from demos/turbine_blade/results/box.json — the constraint box as inline SVG, one card per wall with its verdict at the nominal and at the picked point, each verdict's details.basis verbatim, the open findings (min_fillet_radius, max_section_ratio) named as findings rather than walls, and the root-fillet refusal (E_GEOMETRY/NON_PLANAR_SUPPORT) shown as the kernel finding it is. No behaviour change to any tool; nothing in it re-solves. Structural numbers are published as "meshfree FEA, basis order 2, 8000-cell resolution" — the solver vendor is not named, and the generator refuses to write a page that names it. Assets: eight GLBs converted from the host jobs' STL by the generator (no new dependency), plus the picked point's STEP/STL.
The five walls of the turbine blade, measured. demos/turbine_blade/walls.js gives the first-stage HP blade of a 42 MW-class aero-derivative turbogenerator five independent verdicts as tools/design_loop.js adapters — castability (cad_dfm_check investment_casting, section rule), aero (cad_wind_tunnel, Cd as a ratio to the thinnest-TE point at one grid and one residual target), structural (private cad_fea_case/cad_fea_run: sliding fir-tree flanks + a lockplate face, ω²·r_cg body load at 44,900 g, SF read on the NECK net section from the solver's surface field — the global peak is a fixture-edge singularity and is reported beside it), rim load (cad_mass_properties → new ops/discStress.js, Timoshenko rotating annulus + Lamé rim tension, Robinson average-stress burst margin, pinned to three closed-form limits) and tip clearance (cad_tolerance_stack, judged on RSS, worst case reported) — with targets and alloy data read from the turbomachinery pack. Run on the host as one job per point and folded by demos/turbine_blade/collect_results.js into results/box.json: castability boxes tTE from below (fails 1.2, passes 1.6 — the nominal 0.9 mm edge is uncastable at the 1.0 mm floor); structural boxes the neck from below (SF 1.00 at 7, 1.62 at 9, 1.88 at 11, 2.50 at 13); tip clearance boxes the height from BOTH sides to [59.5, 60.5); the rim load sits inside at 1.32–1.33 everywhere; the aero wall cannot see a 0.4–2.0 mm trailing edge at a 4.3 mm cell (Cd flat to ±5 %, no trend) and says so. The nominal fails one wall; the point picked inside (tTE 1.6, neck 9, h 60) passes all five (1.49 mm / 0.95 / SF 1.53 / 1.32 / 0.75 mm). Along the way the driver learned to give each rebuild a fresh session (a sweep into one session slows quadratically — s → 462 s per point, then a timeout) and to resolve its output dir absolutely. tests/test_cadsf-1200.js (29).
CADSOLVER_REPRO_DUMP=1 no longer goes silently inert under CADSOLVER_TELEMETRY=off. src/kernel-host/repro.js's dumpEnabled() used to treat the telemetry opt-out (and DO_NOT_TRACK) as a privacy veto over the repro dump too. The dump is local-only — tmp/repro/<ts>/{ops.json, verdict.json[, body.step]}, never uploaded — so it isn't telemetry, but the bench runners and the design-loop driver force CADSOLVER_TELEMETRY=off, which meant every refusal they hit produced zero repros. The dump is now governed only by its own CADSOLVER_REPRO_DUMP (default unchanged: on); DO_NOT_TRACK / CADSOLVER_TELEMETRY=off still veto anything that uploads, just not this. No caller-visible error vocabulary changed. See docs/DESIGN_LOOPS.md and benchmarks/text-to-nativecad/README.md.
Portfolio coverage model gains investment_casting. The ops/dfmRules.js PACKS.investment_casting (lost-wax, nickel-superalloy aerofoil class) had no entry in tools/portfolio_coverage.js PROCESS_FAMILIES or TRIGGERS, so the model's own integrity check was red — the tests/test_cadsf-675.js caught it. It now carries a 3% [DERIVED] family weight (cited and derived in docs/portfolio/PART_LANDSCAPE.md §2.3; the other six families rebalance by × 0.97 to keep the table summing to 1), two archetypes (cast turbine blade/aerofoil, cast valve/pump housing), a superalloy-nickel / stainless-cast material split, and TRIGGERS for its four rules — min_section reuses the body-general wall-thickness pattern (always triggers, like min_wall_thickness), min_trailing_edge_thickness triggers on thin_wall, min_fillet_radius on internal_corner, and max_section_ratio on a new section_taper feature class (no existing class named a thick/thin section transition). The greedy selection grows from N=15 to N=16 (12 core + 4 repair); ic_turbine_blade_inconel earns a core slot on its own weight since no other candidate can ever trigger this pack's rules.
Quieter tessellation logs. The per-face [CURVED-TESS] and [CDT-DEBUG] BoundaryEdges lines are DEBUG now (one INFO line per curved face was printed on every tessellation); WARN/ERROR retry and recovery lines are unchanged.
Design loops: one driver for target → build → verdict → remedy → iterate → stop. tools/design_loop.js runs the loop docs/AGENT_FEA_LOOP.md prescribes for stress against ANY verdict tool — cad_dfm_check, cad_moldability, cad_mass_properties, cad_tolerance_stack, cad_assembly_fit, cad_wind_tunnel, and privately cad_fea_run — through the in-process MCP surface, in the knowledge packs' recipe format ({tool,args} steps, $N handles). A wall is an async (ctx) => {pass, measured, limit, margin, remedy?}; the sweep policy scans one variable until the verdict flips and bisects to the WALL (not "fail" but "you are 0.15 mm short"), apply-remedy sends a verdict's complete {tool,args} to the live body, and a failing verdict with no mechanical remedy stops the loop honestly. Every iteration lands in loop.json (params, every step with its ms/handle/refusal code, every verdict, the decision) + a loop.md table; --replay re-runs the final iteration after a kernel change. Two instances ship: dfm_wall (machined tray, floor thickened until machining min_wall_thickness passes, wall bracketed in [0.7, 0.8] mm) and the private bracket_fea (the doc's L-bracket radius ladder as MCP calls, faces named by cad_body_topology normals + cad_measure ray_fire, solved on the host). The server instructions gain "A verdict is an instruction" (order 85); tools/fea/host_jobs.js allow-lists the driver and its test as repo jobs. docs/DESIGN_LOOPS.md; tests/test_cadsf-1190.js.
Shipped in compiled form only. Both artifacts — the npm tarball and the .mcpb — now carry ONE minified dist/server.min.js instead of readable src/** and kernel/** trees (and, in the .mcpb, instead of a bundled node_modules/): the tool modules, the CAD kernel require-closure and the three runtime dependencies are compiled in, with only Node's built-ins external. This is what LICENSE/COMMERCIAL.md have promised since an earlier release. Nothing changes on the wire: same tool inventory, same schemas, same error vocabulary, and CADSOLVER_MCP_TOOL_PATH third-party tool directories still load from disk. A host config that spawns nativecad-mcp / cadsolver-mcp is unaffected; the .mcpb manifest now names dist/server.min.js as its entry_point. cad_server_info's stagedKernel stamp still dates the kernel an install ships — it travels inside the bundle now, and is also readable in the file's banner. See docs/RELEASING.md.
cad_fillet_edges groups a large selective edge report instead of ever refusing a successful build for its own report's size. A big hand- or script-picked edges:[...] selection (no partition, since that only exists on the reentrant/blanket path) built a real, watertight body, but its report.edgeReport — one row per edge, plus a reason/group on any edge a multi-run split could not carry — measured 19+ KB on a 40-edge pick, over the "modeling" family's 16 KiB budget, so registry.js's enforceByteBudget refused an operation that had already succeeded. src/tools/modeling/index.js's new compactEdgeReport now groups rows by status + reason (id-neutralised, matching compactPartition's existing idiom) once the array is comfortably past a hand-picked few edges (>16 rows), into {...sharedFields, count, edgeIds: [first 5], truncated?} — deliberately NOT keyed on the kernel's own per-run group index, which a large pick often sets to a different value per edge and would otherwise defeat grouping entirely. A small selection (cadsb-143.test.js's pinned 8-edge case) is byte-identical to before this ticket — grouping never engages below the threshold. ops/blend*.js untouched; the fix is purely result-shaping.
The governor never suggests an unusable maxTriangles:0 retry. Its byte-budget refusal computed suggestion.maxTriangles as floor(bytesFree / TRIANGLE_BYTES), which floors to 0 when a document's baseline parse/executor cost alone already exceeds the headroom left in the budget — a "reduce maxTriangles to ~0" suggestion no smaller value can ever satisfy. src/governor/index.js's checkHeadroom now omits suggestion entirely and returns recoverable:false with a hint that says plainly the document cannot fit at ANY triangle count, whenever the computed value would be <= 0; a genuine fit still returns a real positive suggestion with recoverable:true, unchanged. src/tools/output/governor.js passes recoverable:false through to the thrown E_LIMIT. src/remedies.js needed no change — its retry branch already only builds a retry when suggestion.maxTriangles > 0. (Also fixed a latent gap in the pre-existing governor.test.js: its "hard cap" case used a byte budget so tiny it was already the "cannot fit" case, so its old assertion that maxTriangles was "a number" was trivially true for a worthless 0.)
cad_export_body maps unwritable-path errors to E_IO, not E_INTERNAL. A write that failed with a plain Node fs error (ENOENT, EACCES, ENOTDIR, EPERM, EISDIR) was an ordinary Error, not a CadMcpError — toEnvelope() downgrades any such error to E_INTERNAL ("this looks like a bug in NativeCAD"), the wrong signal for "the path you gave isn't writable," and one src/remedies.js's filesystem hint never even saw. The export handler now catches its own fs errors (a new rethrowExportFsError, keyed on err.code) and re-raises them as E_IO (already recoverable:true in the code vocabulary) with the ORIGINAL message text preserved verbatim and details.errno/details.path attached — the existing filesystemHint in remedies.js now fires correctly since the code it pattern-matches on is finally right.
cad_pad takes the same profile primitives as cad_extrude_profile, and imprints their arcs exactly. A pad's footprint used to be a list of points, so the only way to ask for a round boss on a face was to sample it — a 24-gon wall where a cylinder belongs, on the ONE verb whose premise is that the profile already lies on the target face. profile.outer (and the top-level outer) now accept one {circle} / {slot} / {rect+cornerRadius} primitive, expanded by the same commands/profile-primitives.js the extrude case uses, and ops/pad.js imprints the result as real CIRCLE edges with CYLINDRICAL_SURFACE walls: a {circle} boss is one cylinder — two half faces — and its volume is exactly πr²·length, matching what cad_extrude_profile has produced since an earlier release. A pocket (negative length) gets the same treatment; its arcs run the other way about the rail, which is the only difference between the two modes. A sketch whose entities curve is now resolved through the curve-preserving path too, so {sketch:"…"} pads round where it used to pad faceted. The result carries pad.hasCurve and pad.cylindricalWalls when the footprint curved (absent otherwise), and the same profilePrimitives note the extrude case returns. Refusals are unchanged in vocabulary: a non-planar target is nonplanar_host from BOTH entry points (the inline spelling used to surface a bare planeFrameFromFace message with no code), and a primitive that describes nothing still refuses with its PROFILE_PRIMITIVE_* code. A straight-sided pad or pocket is byte-identical to the previous release — same STEP, same analytic figures, no new report fields.
cad_dfm_check accepts maxIssues/issueOffset to group and page a large findings document. A real part's rule-pack verdict can carry one finding PER OFFENDING ENTITY — a real SendCutSend route measured 309 KB against the "report" family's 128 KiB response budget. maxIssues now groups every finding (validity + rule-pack) by {severity, reasonCode} into one row with a count and a small entityRefs sample, pages the GROUPS (issueOffset + page.nextOffset), and caps each rulePack.rules[] entry's own violations sample too (violationCount/violationsTruncated) — the second array that scales with entity count and that grouping findings[] alone would not shrink. Omitting maxIssues is byte-for-byte the unbounded legacy shape. Separately, a byte-budget-family E_LIMIT (registry.js's enforceByteBudget) used to carry a hint written for the whole family ("...a maxIssues/maxNodes cap...") even when the FAILING tool had neither — src/remedies.js now appends that tool's own actual narrowing arguments, mined from its schema, and src/budgets.js's report family hint no longer hardcodes argument names not every member has. docs/mcp-tools.json / web/mcp-tools.html regenerated. Note: cad_dfm_check's published schema was already ~13.4 KB (over tool-list-budget.test.js's 12 KB top-3 line) before this ticket, from the shared commands/schema/analysis-ops.js op description and its 29 per-rule rules overrides — out of this ticket's scope to edit; the two new properties add ~360 B on top of that pre-existing overage.
PMI and datum attachments bind by NAME, not by entity id. cad_pmi_author stored its targets as raw face/edge ids, so an annotation survived a rebuild only when the rebuild happened to reproduce the id for the same face — luck, not identity. On the own box, dropping one edge from a fillet's pick list makes #276 name a different fillet, and dropping two makes it name a box wall: the old code kept the annotation, silently, in both cases. Each target is now stored as the id you gave plus the persistent reference minted beside it (ops/refresolve.js's captureRef, with stableTag), and every read re-derives the id from that reference. Three things change for a caller: cad_pmi_author's response echoes item.targets[].ref ({tag, stableTag} — the fingerprint stays on the document); cad_pmi_list gains resolution.rebound ({itemId, from, to, stage} for every attachment whose id moved — a fillet → chamfer swap now follows the face instead of dropping the annotation) and resolution.refused ({code:'persistent-name-mismatch', expected, found} where the only available answer carries a different persistent name — refused, and the item dropped, rather than re-pointed onto a wall); and a { ref } face selector on cad_datum_plane/_axis/_point refuses the same way rather than building a datum on different geometry, reporting a rebind on the feature-tree row as refRebinds. The wire format is additive: a set authored before this ticket carries no references, takes the old "is the id live?" path, and nothing new is even loaded for it. See docs/TOPOLOGY_NAMING.md § 6.
Parametric HP turbine blade demo, and the three refusals it walked into. demos/turbine_blade/build_blade.js builds a first-stage 42 MW-class turbine rotor blade — a cubic cad_loft_profiles airfoil (3 twisted, tapered sections of 59 points) unioned onto an analytic platform, shank and 3-lobe fir-tree root — over {tTE, neckWidth, rootFilletR, bladeHeight}, and writes STEP + STL + a census per parameter point. Nothing in the tool surface changed; what is new for a caller is the recorded behaviour of three calls, all in demos/turbine_blade/README.md with repro paths: (1) cad_fillet_edges on an airfoil-to-platform root loop refuses E_GEOMETRY "only defined between PLANAR faces" even for a single edge — the plane × B-spline blend is convex-only and a root fillet is reentrant (BLEND_PS_EDGE_NOT_CONVEX), so the demo ships with no root fillet rather than faking one; (2) a B-spline solid piercing a plane refuses E_GEOMETRY (splitByPlane: unsupported crossing edge curve B_SPLINE_CURVE_WITH_KNOTS, then genBool's SSI march declines) where the same solid seated flush on that plane unions exactly and in ~2.4 s; (3) cad_body_topology on the finished 975-edge body is 917 bytes over the 512 KiB topology budget with include:['edges'] — include:['faces'] fits. The generator also polices the open over-removal bug itself: every fillet's |ΔV| is checked against r²(1−π/4)·ΣL and a body over 1.5× the bound is refused and rebuilt without that fillet.
Investment-casting DFM process, and a turbomachinery knowledge pack to check it against. cad_dfm_check {process:'investment_casting'} adds four rules for lost-wax nickel-superalloy castings, each reusing the pack system's existing tessellation probe rather than a new one: min_section (delegates verbatim to min_wall_thickness, default 1.0 mm), min_trailing_ edge_thickness (a knife-edge dihedral detector — a face pair whose normals are within 30° of antiparallel reports thickness 0 by construction, independent of mesh density; default 0.6 mm), min_fillet_radius (unblended internal/concave edges via the existing corner detectors, default 0.5 mm) and max_section_ratio (thickest sampled section over thinnest, default 6:1) — all four name the edge id (a sorted, pipe-joined face-id pair — new edgeRef() helper) plus both adjacent faces, where an older rule would have named only a face. min_draft_angle is explicit not_applicable: the ceramic shell is broken away from the casting rather than drawn from a rigid die. data/dfm/packs/turbomachinery.json ships alongside it — 44 entries (16 core / 42 standard) covering first-stage HP rotor blade proportions for both aero-derivative (330 mm hub, 60 mm span, ~11,000 rpm, ~48,000 g) and industrial-frame (500–900 mm hub, 3,000/3,600 rpm) turbine classes, fir-tree root and platform geometry, tip-clearance tolerance-chain contributors, IN718/IN738LC properties at 20/650/850°C, and 3 kernel-verified recipes (fir-tree root, platform+shank block, disc sector with a rim slot). commands/schema/analysis-ops.js's hardcoded DFM_PROCESSES/DFM_RULES enums — the one other place this vocabulary is transcribed — are extended in the same order.
Text to NativeCAD prompt bank and scorecard. benchmarks/text-to-nativecad/ holds 85 natural-language part prompts across 24 sectors — 70 recipes lifted from the manufacturing knowledge packs, 15 hand-authored around the native circle/slot/rect profile primitives — each with an acceptance block (watertight, volume band, bounding box, face-type census, DFM verdict). run_replay.js scores the kernel with no model in the loop; run_live.js scores a real agent over stdio against the same blocks and skips cleanly without ANTHROPIC_API_KEY; score.js publishes web/text-to-nativecad.html, which renders offline. First baseline (2026-09-05): 79/85 accepted (92.9%), 84 built, 3.81 tool calls per part; the six reds are real kernel findings, filed as tickets.
tools/list is 15% smaller — 387 KiB / 3813 B per tool → 330 KiB / 3246 B per tool for the same 104 tools. The bloat was never prose: it was the SAME sub-schema inlined 2–25× inside one tool (cad_dfm_check's 25 per-rule overrides, the fillet/chamfer edge selector inlined inside its own array branch, the extrude profile shape 3×). registry.js now folds a tool's byte-identical repeated subtrees into $defs/$ref at define time, on the same clone that is validated and published. No property, enum value, default or required flag was removed; full prose remains in cad_help. A host whose schema reader cannot follow an internal $ref should say so — the conformance lint treats internal refs as a warning, not an error.
Robustness page: tolerant_ok. A body that is watertight under its per-entity tolerance but not under the global eps is now its own class beside watertight / not_watertight. Re-sweeping the pinned 150-file subset: 5 of 21 not_watertight files reclassify; the per-file max-tolerance histogram (1e-6 … 1e-1 mm; widest 5.75e-2 mm) and before/after counts render offline in a new section of web/robustness.html, with the delta in web/robustness/history.json. tools/tolerance_reclass_sweep.js is chunked and resumable.
A fillet on a PLANE × B-spline edge is built by the scoped path now, and an open spine names the narrow face. A selective fillet on ONE convex edge between a planar face and a B_SPLINE_SURFACE_WITH_KNOTS face is offered to ops/blendPlaneSpline.js (a marched spine, no constructed offset patchand a MEASURED G1) before the general rolling ball; the result reports method: "plane-spline-rolling-ball" with the two supports it resolved. Nothing else moves: blendNurbsEdge remains the path for spline × spline, for chains and for every case the scoped path declines — including its own G1 refusal, so a radius that built before still builds. Measured on the B-spline-faced corpus files: fillet_single_edge 1.69% → 1.69%, fillet_edge_chain 2.70% → 2.70%, fillet_concave 0% → 0% okPct, no row's status or code changed. New caller-visible refusal on that pair: BLEND_PS_SPINE_OPEN (in place of BLEND_NURBS_SPINE_OPEN where both agree), whose message now names the face that ran out — "face #148 is 2.3744 mm wide; radius must be < 2.3744" — with narrowFace / narrowFaceWidth on the error. The width is a measurement of the face itself, walked across the surface from the picked edge and clipped to the face's own boundary rather than to the padded patch the kernel evaluates a plane over.
Stable face/edge identity across a blend re-solve. Editing a fillet radius, or swapping a fillet for a chamfer, no longer moves the names downstream consumers hold. Dress-up features emit a PERSISTENT name derived from their INPUT rather than from the position of the face in the result: a blend face is named after the source edge it consumed (f:blend[edge=…]), a spring edge after (source edge, support face), a corner patch after its source edges, and an untouched face keeps the name it had before the feature ran. Fixes a silent re-point in which f:side[0] named a box wall on a fillet and a chamfer flat after the swap, at full resolver confidence. ops/refresolve.js consults the persistent name first, but only for references that carry one, so every reference minted before this release resolves exactly as it did (naming map version unchanged). cad_fillet_edges / cad_chamfer_edges results carry a persistentNames count. See docs/TOPOLOGY_NAMING.md.
Booleans against B-spline faces no longer lose volume to a one-quad tiling. ops/faceSplit.js sized a chart's interior grid from two straight mid-line probes; on a twisted ruled patch — what a two-section loft between misaligned polygons actually is — both read zero and the whole face was tiled as a single bilinear quad, so a watertight, gate-passing union could be >1% wrong (measured 2.7% on a lofted hull with a radial crossbar). The grid is now sized in 2-D against the diagonal chord, a B-spline face's chord budget is measured from its sampled principal curvature instead of guessed, and any face pair with a B-spline member is held to an absolute 1e-4 mm chord ceiling. Analytic pairs are byte-identical. New nightly boolean family O: 20 seeded loft × primitive operand pairs (tools/loft_operand_pool.js); the volume identity on that pool is pinned at 6e-3 relative — the bound this engine reaches while it bakes curved fragments into planar triangles.
Every coded refusal now names a next step, and a mechanical one ships the corrected call. Additive only — no error code and no error message changed, so anything keying off either is unaffected. Two things are new on the wire. (1) A refusal that previously fell back to its code's generic hint ("details.issues names the exact JSON path that failed") now carries a sentence built from what the envelope already knew: the schema's own field description and the value you sent for an E_INVALID_ARGS; the operator remedy note (tools/operator_remedy_table.js) for a kernel refusal; the errno for a filesystem failure. Measured over tools/audit_refusal_hints.js's matrix — one canonical wrong call per code class per tool family — specific hints went from 17/35 (48.6%) to 35/35 (100%). (2) error.retry — {tool, args}, where args is a complete argument object you may send verbatim, never a patch to merge (a merge-shaped field would be named argsPatch; this server emits none). It appears only where the remedy is mechanical: a mistyped tool name the registry already corrected (details.didYouMean), arguments rejected solely for fields the tool does not have (the fix is deletion), and an E_LIMIT the governor already solved (details.suggestion). All 6 mechanical rows in the matrix carry one and all 6 succeed when applied, asserted end-to-end in test/e2e/scenario-c-error-paths.test.js. retry is declared as an optional property on errorEnvelopeSchema(). See docs/REFUSAL_HINT_COVERAGE.md.
Native sketch primitives in profiles — stop sampling N-gons. Any profile loop of cad_extrude_profile (outer, holes, and profile's own pair) and cad_revolve_profile's profile may now be a primitive object instead of a point list: {"circle":{"cx","cy","r"}} (d accepted for r), {"slot":{"x1","y1","x2","y2","width"}} — the two points are the END-ARC CENTRES and width is the full width, so the overall length is |P2-P1| + width; the centre form {"cx","cy","length","width","angle"} takes length as the OVERALL tip-to-tip length (> width) and angle in radians — and {"rect":{"x","y","w","h","cornerRadius"}}, where x,y is the centre, not a corner. A primitive is a WHOLE closed loop; mixing one into a list of points is refused. The geometry is EXACT: a circle extrudes to one CYLINDRICAL_SURFACE bore (two half-faces sharing two seam edges, the same construction cad_cylinder uses) and revolves to a real torus of TOROIDAL_SURFACE legs, with volume equal to π r² h / 2π² R r² to 1e-9 relative. That removes the ~1% area error, the N-per-hole facet fan that made later booleans and fillets fragile, and the hand-written winding. Winding is no longer the caller's problem at all: in a primitive-bearing profile the role of every loop is inferred by containment (nested = hole) and reported back in the result's profilePrimitives.note, so a "hole" put in outer is silently corrected — visibly, in the result. New machine-branchable refusals: PROFILE_PRIMITIVE_MIXED, PROFILE_PRIMITIVE_FIELD, PROFILE_PRIMITIVE_DEGENERATE, PROFILE_PRIMITIVE_AMBIGUOUS, PROFILE_LOOP_SHAPE, PROFILE_ROLE_AMBIGUOUS, REVOLVE_TORUS_DEGENERATE, REVOLVE_PROFILE_CROSSES_AXIS, REVOLVE_PRIMITIVE_ARCS_CONFLICT. The server instructions and the extrude/revolve tool descriptions now lead with this and demote the "approximate a circle with a 24..32-gon" advice to a fallback for shapes that genuinely have no primitive. Behaviour to be aware of: cad_pad is unchanged and still wants a sampled polygon (its imprint path is polyline-based), and a polyline-only document produces byte-identical STEP.
Profile loops are auto-oriented — a reversed hole no longer fails two calls later on watertightness. An earlier change already fixed this for a primitive-bearing profile; this extends the SAME normaliser (commands/profile-primitives.js's expandProfile()) to every plain-polygon outer/holes loop cad_extrude_profile and cad_revolve_profile see. Every loop is classified by containment depth (even = outer, odd = hole), a loop whose signed area disagrees with its role is reversed, and the result carries a notes[] entry ({loop, flipped:true, reason}) for each one actually flipped — an already-correctly-wound profile gets an empty notes[] and byte-identical STEP output. A loop that crosses itself has no well-defined orientation and is refused by code, E_PROFILE_SELF_INTERSECTING, rather than guessed at. Opt out per-op with orient:false (a primitive-bearing profile still needs expansion to exist at all, so the flag has no effect there). cad_revolve_profile's r-z silhouette already normalized its own winding internally (ops/revolve.js's normalizeProfile()); this adds the same notes[] report and self-intersection refusal on top of that, without changing what gets built. cad_pad is unchanged (its imprint path is polyline-only, a follow-up own note). See tests/test_cadsf-1167.js.
The session survives a kernel crash — an op journal, and a replay. Every accepted op is appended to a per-session NDJSON journal under the OS temp directory (path in cad_server_info's new session.journal), one line per op with its args and the handles it produced, plus an in-flight line written before the handler runs. After a kernel death or a restart the journal is replayed: the documents come back, and every body handle is re-bound under the string it already had — ab12_solid_7 is still ab12_solid_7, so an agent holding forty handles from before the crash holds forty working handles after it. The op that was in flight is NOT replayed; it is recorded as poison with the error that killed it, and the call that died is refused with the existing E_KERNEL_CRASHED (no new code) and the hint "the document was restored; the op was not applied". cad_list_model_state gains restored ({restored:true, at, journal, documents, ops, handles, poison}, null when this listing was not rebuilt) and journal. A journal older than the store's idle session TTL (1 h) is discarded rather than replayed — a restart is a continuation, coming back tomorrow is not. CADSOLVER_NO_JOURNAL=1 turns the whole mechanism off; CADSOLVER_MCP_JOURNAL_DIR repoints the directory and CADSOLVER_MCP_JOURNAL_MAX_AGE_MS sets the window (0 = forever). Behaviour change to be aware of: a pre-restart handle that used to fail E_NOT_FOUND after a restart now RESOLVES, to the same geometry it named, because the document behind it was rebuilt from the same ops in the same order. A re-exported tolerantly-stitched import declares the tolerance it was judged at. cad_export_body to STEP wrote the 0.01 mm baseline into UNCERTAINTY_MEASURE_WITH_UNIT even when the import's seams had been accepted watertight only because Topology widened them over a gap (0.05 mm, say) — the widening lived on the Topology, not in the per-entity tolerance stores the writer read. The header now also folds in the widest widened seam (only tolerances above the model's own eps count, so a plain body's output is byte-identical), and a reader honouring GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT re-imports the body at the tolerance it was built at rather than 5× tighter. Faces whose file never marks an outer bound now blend and chamfer. STEP makes FACE_OUTER_BOUND optional and KiCad's exporter never writes it — every face of every KiCad footprint arrives with plain FACE_BOUNDs only — so cad_fillet_edges / cad_chamfer_edges on such a part refused blend-sel-face-no-outer-bound / chamfer-sel-face-no-outer-bound (~320 rows on the robustness subset) the moment they rebuilt a face with a hole in it. Topology now infers the outer loop once, at import, where the file is silent: the loop whose bounding box contains every other loop's, else the largest planar area, else the largest extent; a face with two equally large boundary loops (a seamless periodic band) is left unclassified and marked ambiguous rather than guessed. A face the file did flag is never second-guessed. Rebuilt faces are written back with an explicit FACE_OUTER_BOUND. On 16 of those files both codes went 54 → 0 and 40 of 78 blend/chamfer attempts now build clean (14 more build carrying only defects the import already had).
Fillets and chamfers on imports that author .F. FACE_BOUNDs now behave exactly as on .T. ones. STEP composes two direction flags — the ORIENTED_EDGE's and the FACE_BOUND's — and KiCad/FreeCAD footprints and NIST ctc_04/ftc_10 write the bound flag .F.. Several blend/chamfer sites still read the ORIENTED_EDGE flag alone: the whole-body and selective fillet classified every edge of such a face with the OPPOSITE convexity, put the rolling ball on the wrong side (so cad_fillet_edges refused "radius is larger than the feature next to edge" on a fit that was fine), cad_chamfer_edges refused "setback runs the wrong way", the blanket chamfer could publish a body with inconsistent face orientation, and the NURBS rolling-ball path re-emitted every untouched .F.-bound face wound backwards — the 25 + 28 orientation-inconsistent refusals on the robustness subset were that, not an import defect. cad_face_edges reported the same inverted convexity. All of these now read the effective direction (Topology's le.sense) and re-emit loops through the canonical .F.→.T. transform (Loop#effectiveEdges, whole-loop reversal). On a body with .T. bounds nothing changes (byte-identical results); on the same body re-authored with .F. bounds every op now builds the same solid to the same volume. The robustness battery's own edge classifier had the bug too, so fillet_single_edge / fillet_concave seeds on those corpus files were sometimes the wrong edge — re-sweep before comparing against the 09-02 baseline. Shell/offset/thicken carry the same raw read and are tracked separately.
New tool cad_assembly_contacts — which faces actually touch. The pre-flight a multi-body model owes any downstream analysis: a solver bonds whatever touches and welds nothing that does not, and neither half is visible in a viewer. Per body pair with overlapping bounding boxes — coplanar PLANE faces with opposing normals report their measured overlap AREA in mm² (exact on the tessellation, holes excluded) and the gap; coaxial CYLINDRICAL faces report signed radial clearance (negative is interference), diametral clearance, axis offset and axial engagement length, with the bore named; everything else reports the exact closest triangle pair and both face ids. unbonded lists every body touching nothing, with its nearest measured neighbour. Reads NAUO product structure when the file has it, one instance per solid when it does not. READ-ONLY (ops/contacts.js); a 149-body assembly takes ~4 s.
Tolerant topology, end to end. Every edge, vertex and face carries a tol (never null; the global eps by default, the file's declared UNCERTAINTY_MEASURE_WITH_UNIT on an imported body). An edge or vertex merged by stitch — and so by every boolean and heal — inherits max(tol) instead of the survivor's tighter claim; SSI publishes the tolerance a trace actually achieved and the boolean declares it on the edge it snapped to. cad_body_topology reports per-edge tol plus {eps, maxTol, maxTolEdgeId, countAboveEps}; STEP's global uncertainty carries the widest of those numbers and a reader restores it onto every edge. docs/TOLERANT_TOPOLOGY.md.
Fillet — rolling-ball blend between a planar face and a B-spline face. The spine is marched directly: the plane's offset is exact and unbounded and the B-spline constraint is evaluated by Newton point inversion instead of by intersecting two constructed offset patches — 2.5× faster and a tighter seam than the general path, which stays available as spineMode:'ssi'. The blend is emitted as an exact bicubic b_spline_surface_with_knots band with spring-curve trims of both supports, and its tangency to them is measured on the authored surface (50 samples per seam, refused past 1e-3 rad). Scoped to one convex plane/B-spline edge; chains, vertex blends and every other support pair refuse by code (BLEND_PS_*).
A fillet whose band would cross a hole is refused before it is built, by name. cad_fillet_edges on an edge whose blend footprint runs over a bore mouth, a pocket or an island root on one of its support faces now refuses with blend-sel-footprint-crossed, naming the crossing edge(s) (crossingEdgeIds) and the largest radius whose footprint clears the feature (measuredFeatureSize, also in the message), and the input body is untouched. Before, the blend's dry run polygonised face loops from edge vertices only — a circular hole mouth was a two-point polygon it could not see — and its reach test was a ray at three stations along the edge, so a hole anywhere else was missed: the band was laid straight across it and the result self-intersected. On the parity-B5 body (bore at mid-span) the mesh-tier gate caught that after the fact with a "27 duplicated windings" message; with the bore at x = 10 or 33, or as a blind pocket, the same call returned ok: true and only cad_dfm_check's full tier found the two faces overlapping. Those calls refuse now. A hole clear of the band, and every legitimate fillet, builds exactly as before; a notch in a face's OUTER boundary between the three ray stations is now seen too and refuses as blend-sel-radius-overshoots-neighbour with its own measuredFeatureSize. Trimming the band against the hole (so these calls build) is follow-up work.
cad_measure gains a section kind. Slice a solid with a plane and get area, centroid, second moments (Ixx/Iyy/Ixy), principal axes and the torsion constant J (polar approximation for solid sections, thin-wall Bredt for closed hollow ones — jMethod says which). Catches a structural member extruded from its plan outline instead of its cross-section before any downstream analysis runs on a wrong I.
cad_tessellate_body / cad_mesh_payload gain maxEdgeLength and minTrianglesPerFace. A conforming (watertight-preserving) midpoint-split refinement runs after the chordal pass; new vertices sit on the chord midpoint with normals read from the analytic surface where one exists. Face ids are preserved per triangle. Omitting both keeps the mesh byte-identical.
Third-party ops can join the tool surface: the kernel plugin SDK. New @nativecad/kernel-sdk (packages/kernel-sdk) is the semver'd surface an external package builds a feature op against — createDocument, registerOp, ops, topology, export, tessellate, license, with opaque body handles that carry no kernel entity id — plus an in-process transport and a JSON-RPC-over-stdio host (nativecad-kernel-sdk) for a caller that is not written in JavaScript. Set CADSOLVER_PLUGINS to a comma-separated list of packages or paths and each plugin's ops mount under their own namespace: gears.spur appears in tools/list, runs in a fresh document, and reports its topology plus the STEP it produced (pass _export to have the file written). Refusals use the SAME error.code vocabulary as the built-in tools, so nothing about error handling changes for a caller. CADSOLVER_PLUGIN_TOOL_SEPARATOR=_ renames gears.spur to gears_spur for a host whose tool-name grammar rejects dots; a plugin that fails to load is warned about and skipped rather than taking the server down. With CADSOLVER_PLUGINS unset the tool surface is unchanged — no new tool, no new bytes in tools/list. Reference plugin: packages/example-plugin-gear (involute spur gear, SDK ops only). Author's guide: docs/PLUGIN_AUTHORS.md.
The certified-MCP program: a conformance suite, a signed registry, and a partner licence tier. tools/conformance/run.js drives ANY MCP server over its own stdio through a fixed case set — primitives against closed-form volumes, the boolean identity V(A∪B) + V(A∩B) = V(A) + V(B) to 0.5%, a blend that has to change the topology, a STEP export that has to re-import watertight at the same volume, four deliberate mistakes that have to come back as structured refusals in the server's own published vocabulary, and a tool-schema lint — scores it 0–100 and prints a readable report naming the measurement behind every failure. It emits a conformance.json hashed over canonical bytes and, with the release key, counter-signed with Ed25519; the public key is committed under tools/conformance/keys/ and run.js verify <file> checks one offline. registry/certified.json lists the servers that passed (name, version, kernelVersion, sdkVersion, manifestHash, date) and is rendered at web/registry/. New for callers: cad_server_info gains a certified block quoting this build's registry entry when a registry is visible (absent in a published install, which ships none), and a plugins block when CADSOLVER_PLUGINS is set. New licence tier: partner — required to mount plugin namespaces outside the public SDK set (today gears) and to boot a kernel snapshot staged on a gated channel; CADSOLVER_PARTNER_NAMESPACES is the development escape hatch. Existing tiers are unchanged, no tool is gated by a licence, and a build with CADSOLVER_PLUGINS unset has exactly the tool surface it had before. Rules, including re-certification and revocation: docs/CERTIFICATION.md.
A refused/thrown op no longer leaves a residue body. A boolean that folds several tools inside one op (cad_boolean_subtract/_union/ _intersect with a tool list) only deleted its inputs after every fold succeeded; if an earlier fold severed the base into several disconnected lumps and a later fold was then refused, the op threw before that cleanup ran — but the severing fold's own new solid(s) were already live in the document and never published to any handle, so cad_export_body's of:"all" (and a plain document-wide export) picked them up as an untracked phantom body. commands/executor.js now rolls a failed op's document state back to exactly what it was before the op ran — no partial credit, no orphaned geometry — whether the op is invoked through the MCP tool surface or batch/cli.js. (The batch demo scripts' keep_result_solid STEP-surgery hack in demos/desk/recipe_lib.py, which existed only to paper over this by keeping the biggest solid in an exported file after the fact, is removed.)
One tool joins the surface (103): cad_design_knowledge. The DFM knowledge-pack database (docs/DFM_KNOWLEDGE_PACKS.md) ships inside the staged kernel — 17 sector packs, 952 entries, 42 kernel-verified recipes — and cad_design_knowledge {action: list | load | search} reads it: search with the user's words, load one sector at size core (paged; standard/full page further; a page over the new 48 KiB knowledge family refuses E_LIMIT naming the size that fits; an unknown sector refuses E_NOT_FOUND with the known list). Server instructions now tell an agent to consult it BEFORE modelling a named product. Steering sentences open the cad_boolean_union, cad_cylinder, cad_fillet_edges and cad_shell descriptions; they go when that follow-up lands. cad_fillet_edges on the partition path no longer returns E_LIMIT for a body that built: skipped-edge reasons are grouped with their #ids neutralised.
Nine object packs join the knowledge database (26 packs, 1379 entries, 69 kernel-verified recipes). Round one was organised by sector; round two is organised by what a text-to-CAD tester actually types. A coverage check of 174 common prompts ("coffee mug", "phone stand", "20-tooth spur gear", "Raspberry Pi case", "planter", "ring size 7") found 106 with no alias or entry; these packs close that gap: desk-everyday, kitchen-tableware, power-transmission, toys-games, maker-enclosures, mounts-brackets, garden-outdoor, jewellery-wearables, desktop-robots (the 2025–26 wave of small companion robots — Reachy Mini, Bittle, SO-101-class arms — with STS3215 servo pockets, round eye-display cut-outs, 18650 bays). Facts already owned by robotics / electronics / consumer are referenced with see, not copied. Every recipe's expect block is what the kernel returned on 2026-09-04.
Housekeeping visible from outside: docs/mcp-tools.json / the tool reference and the web parity matrix are regenerated for the 103-tool surface, and the NURBS and tolerance showcase pages on featureboard.ai carry their data again.
Private (not shipped)
Work that exists in the repo and is deliberately NOT in this package. A private tool group (docs/PRIVATE_TOOLS.md) registers only under an owner licence or CADSOLVER_PRIVATE_TOOLS=<group>, and its directory, kernel modules and assets are excluded from the npm tarball and the .mcpb. If you are reading this from an installed package, nothing below is present in it — the section is here so the history stays honest about what the version contained, not as a menu. cad_server_info remains the authoritative inventory of what you have.
fea — cad_fea_case and cad_fea_run, the stress-analysis loop (1144, 1145, 1146; withheld by). Withheld because cad_fea_run's preferred solver is a partner's product and the commercial conversation has not closed. cad_fea_case turns a built body plus face-selected boundary conditions (fixed / sliding / force / pressure / bearing, faces = cad_body_topology ids, material a preset or SI values) into a case directory: body.stl, one patch_<n>.stl per BC sliced from the SAME tessellation, triangle_faces.json and case.json (docs/FEA_CASE.md v1). Refuses E_INVALID_ARGS with reason unrestrained when no face is fixed, E_NOT_FOUND for a face not on the body. cad_fea_run solves it with the first solver installed — the partner's meshfree solver (needs its wheel) then CalculiX (ccx on PATH or CADSOLVER_CCX; the kernel's own VolumeMesher tets, C3D10) — and returns results.json: max von Mises and where it is, maxVonMisesFace (the kernel face under the peak), displacement, safety factor vs yield, plus a one-line summary. No solver installed ⇒ ok:false, error:"solver_missing" with install advice; a stress number is never fabricated. case.json.provenance is echoed so iterations stay self-describing. docs/AGENT_FEA_LOOP.md walks the bracket loop (tools/fea/bracket_demo.js: inside-corner fillet r = 1 → 6 mm takes the peak from 179 → 44 MPa, peak located on the fillet face each time). Not on the public tool reference, not in the web parity matrix, and no parity release-exception — a withheld tool is not a web gap, it is not a surface at all.
NativeCAD 0.7.x
v0.7.0 “Fischer”
sha256 nativecad-mcp-0.7.0.mcpb cda028f7988db22f908528d8d2cda1adbecdb0061050703a81194abf68e8e7ecsha256 nativecad-mcp-0.7.0.tgz ca5125dc1799f8b0bf02766d36ed4f8e2ef3e2015ef8750a70d70a5197991099One tool joins the surface (102, was 101) and whole-body blends change behaviour. Nothing was removed or renamed; every 0.6.0 call keeps working, but two classes of call now SUCCEED where they used to refuse — read "Behaviour changes" before keying off a blend refusal. cad_server_info remains the live inventory.
New tools
cad_tube_sweep— a closed planar section carried in the path's normal plane along a polyline/arc path: the pipe-and-tube casecad_sweep_profilehad to approximate.
Behaviour changes (calls that used to refuse now build)
cad_fillet_edges/cad_chamfer_edgeswith NOedgesselection (whole body) now partition and continue by default: every convex edge that can take the blend is dressed and the rest are reported underreport.partition.skippedwith a reason code each, instead of one bad edge refusing the whole body. Passedges:[...]for the old all-or-nothing contract on a selection.cad_fillet_edgeson a partial pick of a tangent chain falls back to the whole chain rather than refusingblend-rim-partial-selection; circular edges are now first-class on the selective path; the concave (reentrant) chamfer builds; an island's full root loop blends.cad_boolean_*on cylinder∩cylinder produce an analytic edge, and radial bore patterns stay analytic past the quarter turn.cad_shelloffsets curved faces exactly — plane/cylinder/cone/sphere/torus — and a half-open periodic surface no longer votes its seam.cad_import: orient-shell pass and sewing at import; a hole landing on a wall's seam no longer refuses.
Budgets (new refusals, all recoverable by name)
- Booleans carry
budgetMs(default 120 s) and refuseboolean-budget-exceededwith phase/elapsed/counts; shell likewise (shellBudget, 20 s); tessellation caps CDT sites per face at 60 000 and degrades chord tolerance before refusingtess_site_budget_exceeded. Every bare blend/chamfer throw now carries a reason code; loft and split refusals too.
Response-shape changes
cad_fillet_edges/cad_chamfer_edgeson the whole-body (partition-and-continue) path:report.partition.skippedis now grouped — one{code, reason, edges[], count}per distinct reason instead of one{edge, code, reason}per edge — andreport.edgeReportentries that only repeat whatpartition.blended/partition.skippedalready say are dropped (an emptied array becomes a pointer string). The per-edgeedgeReportarray is unchanged on the selective (edges:[...]) path and whenever there is nopartitionblock. Why: the per-edge prose put a plain 16-edge plate's response at 31 KB against the modeling family's 16 KB budget, so a fillet that BUILT came back asE_LIMIT.
Fixes
cad_design_reviewdrawing sheet: the polyline simplifier deleted geometry — the tip of a doubled-back run (two coincident arcs) and up to 0.1 mm of sag on dense arcs — so a watertight solid's outline drew open at a bend tangent. Now Douglas-Peucker with distance-to-segment, honest to the stated 0.02 mm sheet tolerance; sub-tolerance stubs no longer draw as dots.
NativeCAD 0.6.x
v0.6.0 “Elder”
sha256 nativecad-mcp-0.6.0.mcpb 9d4af9857c2cc5df73cb614350a10b5f1e5669685602ee357942b70509c77f27sha256 nativecad-mcp-0.6.0.tgz a34c6cfd55e46d53862da70c8a05a8f77e703e6847e8dbb2e8a8f621c9734412Five tools join the surface (83, was 78) — sheet metal arrives. Nothing existing was removed or renamed; every 0.5.0 call keeps working. cad_server_info remains the authoritative live inventory, and docs/mcp-tools.json is now generated by booting the real server and taking what tools/list answers.
New tools
cad_sheet_fold— sheet metal: a constant-thickness strip runsbaseFlangeflat, then eachbends[]entry bends (angle in DEGREES — a brake callout, not radians — insideradiusdefaulting to one thickness) and runs itsflange; optionalkFactor(0 < k ≤ 0.5). The developed FLAT PATTERN is reported on the entry, so the blank a laser cuts ships with the folded solid. Geometric refusals are NAMED PER BEND by ops/sheetMetal.js.cad_hole_pattern— hole arrays: linear / grid / circular / points.cad_build_plan— batch plan runner: a spec result, plan, or ops[] in one call.cad_list_model_state— session-recovery listing after an MCP server restart; pairs with the session-epoch fix so stale handle ids from before a restart refuse by name instead of resolving to the wrong body.cad_license— seat status/activation; shipped in 0.5.0 builds but missing from the published manifest until now.
cad_export_body
format: "flat-dxf"— the developed flat pattern of a folded sheet body as DXF.- glTF/GLB: UV unwrapping with TEXCOORD_0, seams on closed surfaces, and multi-LOD export — N tessellation levels in one call.
- Fix (error-vocabulary relevant): A schema default for
lodModehad the same shape as theviewregression; the default is removed, so an export that never mentions LOD no longer takes the LOD path.
cad_import
stitch/stitchTolare pinned by a new field-level drift check over the whole schema surface, so a renamed or retyped field fails the suite the day it happens.
Other
- Windows absolute paths (
C:\...) are recognized on any host OS. _boolProvis internal and no longer leaks into entries.- XT writer message/doc follow-ups for rational NURBS.
- LICENSE/COMMERCIAL.md carry the 2026-08-30 licensing overhaul: every license is an annual term, Excluded Parties (competitors) are ineligible, competitive-use restrictions, removal on termination with downstream flow-down, and evaluations approvable by the licensor's authorized (incl. AI) agents.
NativeCAD 0.5.x
v0.5.0 “Doráti”
sha256 nativecad-mcp-0.5.0.mcpb 948f3cc1ae47f2e4874e79a6cfbf2698bbcf73a5f39a383c5bb48257c6667c39sha256 nativecad-mcp-0.5.0.tgz f44ba1a02c32c2943ce6c7a28dd8c45f97c545cd794fdc8c5bf9aba84cd1cceaNine new export formats. No new tools. The tool surface is the same 78 names 0.4.0 shipped — nothing added, nothing removed, nothing renamed. Everything in this release lands as new formats and options on tools that already existed, so a caller written against 0.4.0 keeps working unchanged; cad_server_info is still the authoritative live inventory.
cad_export_body
format grows from five values to fourteen:
| format | what it writes |
|---|---|
pdf / prc | 3D PDF and raw PRC (ISO 14739-1), with pattern-instance assembly placement |
gltf / glb | glTF 2.0 scene — welded normals from the B-rep, shared meshes for instances, provenance in asset.extras. gltf also reports binPath for its sibling .bin |
x_t / x_b | Parasolid transmit, text and neutral-binary — single-body parts, graph-identical round-trip against XtReader |
3mf | the additive core-spec package |
sat | ACIS SAT |
ap242-xml | AP242 domain-model XML assembly structure plus one Part-21 .stp per part (partPaths); refuses a structure-less document by name (AP242XML_NO_PRODUCT_STRUCTURE) |
New options, all optional and all ignored by the formats they do not apply to (the tool refuses rather than silently ignoring when an option is meaningless for the chosen format):
view: "wireframe" | "hlr"— svg/dxf only."hlr"is vector hidden-line removal: real B-rep edges plus view-dependent silhouettes, occlusion-tested, emitted as dashed-hidden SVG groups / VISIBLE+HIDDEN DXF layers, withvisibleEdgeCount/hiddenEdgeCounton the response. Default output is byte-identical to 0.4.0.creaseAngleDeg,bakeTransforms,animations,materialsOverride— gltf/glb.envelope/envelopeViews— gltf/glb Envelope Export: a measured exterior-visibility cull that ships only the bodies actually visible from outside, with a kept/dropped report.pmiFrom— pdf/prc: graphical PMI carried into PRC markup and annotation views.compression,quantize— gltf/glb, declared but NOT IMPLEMENTED; they are part of a separate change and are described as such in the schema rather than silently accepted.
cad_import
New options heal, stitch, stitchTol, structureOnly, subtree, and new readers behind the existing file-extension routing: ACIS SAT, 3MF, and SLDPRT / Parasolid XT including binary XT extracted by content. New SAT_* reason codes.
cad_boolean_union / _subtract / _intersect
New meshFallback option (default off): when the exact ladder refuses, run the faceted mesh-boolean path with its repair pipeline and leak census, returning a B-rep flagged meshFallback/faceted. Opt-in — nothing changes unless you pass it.
Fixed
cad_export_bodyrefused every non-svg/dxf export. An earlier change gaveviewa schemadefault: "wireframe", andvalidate.jsruns ajv withuseDefaults: true— soargs.viewwas materialised on every call and the "view only applies to svg/dxf" presence guard fired off the default alone. Between an earlier change landing and this release, step, stl, contour-dxf, pdf, prc, gltf, glb, x_t, x_b, 3mf, sat and ap242-xml all returnedE_INVALID_ARGS. Fixed by removing the default;"wireframe"is unchanged as the behavioural default. If you are on a build between those two points, upgrade.
0.4.0 packaging note (2026-08-28)
npm run build:packages works again end to end: the three drifted copies of the browser esbuild flag list are now one sourced file (web/browser-bundle-flags.sh), which unblocks the @nativecad/viewer 0.2.0 dist and its publish. Build-output only — no behavior change in this package.
NativeCAD 0.4.x
v0.4.0 “Dohnányi”
sha256 nativecad-mcp-0.4.0.mcpb efe36f45b1e94cf1d5487d5d45919c0d8e11fd8b1b6f3ce1393db89392677ed3sha256 nativecad-mcp-0.4.0.tgz d4d9af13fced523ac06b546c69958b03ed7129f6f61544f0ac4dc8a292ddffbaThe package is now nativecad-mcp, and the command is nativecad-mcp. NativeCAD (formerly CADSolver) is the same server, the same kernel and the same tool surface under the product's new name (docs/BRAND.md). Nothing about the geometry, the schemas or the error vocabulary changed in this release — this entry is entirely about names.
| before | after | |
|---|---|---|
| npm package | @cadsolver/mcp | nativecad-mcp |
command / npx | cadsolver-mcp | nativecad-mcp |
| MCPB bundle | cadsolver-mcp-<version>.mcpb | nativecad-mcp-<version>.mcpb |
| kernel library | @cadsolver/kernel | @nativecad/kernel (0.2.0) |
| viewer library | @cadsolver/viewer | @nativecad/viewer (0.2.0) |
The package name lost its scope on purpose. npx <name> resolves a registry package, and npm's exec refuses a package with several bins unless one of them is named after the package — so a scoped @nativecad/mcp could not have kept the documented one-liner working next to a compatibility bin. Unscoped, the package name and the primary bin name are the same string, so npx with the bare package name is exactly what it looks like.
Nothing that already works stops working
cadsolver-mcpis still a bin in this package. It is a shim: it writes one line to stderr (never stdout — that is the JSON-RPC wire) saying the command was renamed, then loadsbin/nativecad-mcp.js, which recognises the alias path as a process entry and starts normally. Same server, same exit codes, same stdout bytes. Silence it withNATIVECAD_MCP_NO_DEPRECATION_NOTICE=1if your host treats any stderr output as a boot failure.- An existing MCP host config keeps booting, whether it spawns the bin by name or by path (
node …/bin/cadsolver-mcp.js), and whether it came from an.mcpbbundle built before the rename or a hand-written JSON block. - Every
CADSOLVER_*environment variable is unchanged —CADSOLVER_MCP_LOG_LEVEL,CADSOLVER_MCP_OUTPUT_DIR,CADSOLVER_MCP_TOOL_PATH,CADSOLVER_QUIET,CADSOLVER_MCP_STOCK_STDIO. Renaming them would have broken running installs for no user-visible gain. - Tool names, resource URIs (
cadsolver://…), error codes, the project file extension and the repository URL are unchanged. Internal module, class and variable names are explicitly out of scope (docs/BRAND.md).
One thing a caller may notice
initialize reports serverInfo.name from package.json, so a client that pinned the literal @cadsolver/mcp now sees nativecad-mcp. The same string is stamped as source in emitted robustness/eval artifacts (<name>@<version>). Key off the version, not the name, if you need to branch.
Migration
Change one line in your host config, when convenient:
{
"mcpServers": {
"nativecad": { "command": "npx", "args": ["-y", "nativecad-mcp"] }
}
}
The old block keeps working until the alias bin is removed, which will not happen before the old name has been out of the docs for a full release cycle.
NativeCAD 0.3.x
v0.3.9 “Davis”
Kernel-refresh release: no MCP tool surface changes since 0.3.8; the staged kernel picks up the day's kernel work. One new refusal a caller can hit:
steppedRevolverefuses boundary-shoulder profiles: a profile whose first or last segment is a same-z radius step used to silently build a SOLID at the larger radius (a washer/tube request came back +18.86% heavy); it now throws naming the disc it would have built and pointing at the closed-loop revolve constructions that do support bores. Reachable throughcad_revolve_profile-adjacent shaft tooling only with a malformed profile; every shipped call site is unaffected.- Kernel additions staged but not MCP-exposed:
ops/positioner.js(serial revolute axis chains + coordinated motion),ops/fixtureKit.js/ops/fixtureAudit.js(parametric fixtures + 3-2-1 fit audit),ops/weldWeave.js(weave patterns; sub-pitch seams report the ACHIEVED schedule),ops/productionReport.js(cycle-time/production reports). MassPropertiesverified against closed forms for hollow revolves (tube/washer/tapered-bore, <0.5% chordal) as part of a separate change — no behavior change, the suspicion was disproven.
v0.3.8 “Chung”
Everything under "Unreleased" below ships in this build, plus the day's kernel work it stages (kernel tree 89682d8fcafb…):
- Import is 3.9× faster corpus-wide: Shewchuk stage-A filter in front of the exact in-circle predicate, shared-Topology health checks, one topology per import batch. The 9.2 MB cadsf-519 dice-inlay file: full
importverb ~100 s → ~12 s. Geometry byte-identical (per-solid SHA-256 proven across the corpus). - STEP exports carry real body names: per-solid
PRODUCT/NEXT_ASSEMBLY_USAGE_OCCURRENCE, read back on import;cad_importsurfacesnamesper solid. Files written with no names are byte-identical to 0.3.7's output. - Units end-to-end:
cad_import/cad_saveacceptunits(mm/cm/m/in/ft/um); declared file units are honored by default — an inch-declaring STEP no longer imports 25.4× too small. planar_sheet+ grouped blends + CFD knobs in the command schema (schema 1.9.0); blend-on-blend band terminals build instead of refusing;cad_fillet_edgesacceptsperEdge/order.- Cost model:
docs/COMPLEXITY.md+ opt-in per-opcostModelreporting (CADSOLVER_COST_MODEL=1).
Fixed
scripts/stage-kernel.js/plugin/build.js: staging degrades to overwrite-in-place where unlink is refused (CIFS mounts), failing hard if a stale file would survive; the review tool group's documented browser-branch require is allowlisted (staging was refusing to run at all).
NativeCAD Earlier notes
Unreleased
Added
- Document versions —
cad_snapshot {name, document?},cad_branch {from, name},cad_restore {version, document?},cad_versions {document?}. Lewis: "keep a few versions of something in memory as we do simulation and then keep modifying it without reloading." A version is a frozen copy of the document's op list and handle table (the op list IS the model, so a snapshot is a JSON copy; the B-rep is never stored) plus{bbox, volume, bodies}measured when it was taken.cad_branchforks a NEW document from a version — the ops replayed, every body under a fresh handle (handlesmaps old to new), the original untouched — and gives it its own baseline version (parent= the forked one).cad_restorerewinds a document to one of its versions: handles minted after it are released (they stop resolving), handles it had are re-bound under their original names, the kernel worker's copy is rebuilt from 0.cad_versionslists a document's versions newest first withcurrent(the version the document sits at exactly) and, per version, the simulation results recorded while the document sat there: a successfulcad_wind_tunnel/cad_fea_*call now attaches a bounded digest of its result to the document's current version (nothing in the session kept those numbers per document before). Journal-backed — newversion,restoreandresultrecords — so versions and rewinds survive a restart;cad_session_resetdiscards them with the documents. New codesE_VERSION_UNKNOWNandE_VERSION_EXISTS(both recoverable); restoring a version of another document isE_CONFLICT(branch it instead). cad_delete_body {body, document?}andcad_batch {atomic:true}. Nothing on the surface could remove a body: a model that re-sent one failing batch three times left two orphan seats behind, and the only remedy wascad_session_reset.cad_delete_bodytakes a body/sheet/mesh/group/sketch handle (or a datum op's id), removes the op that produced it from the document's op list, rebuilds the rest, releases its handles and rewinds the op journal — so the body stays gone across every later replay and does not come back on a crash restore. New codeE_BODY_IN_USE(recoverable): a later op depends on the body — delete the dependents first, last to first. One body of a multi-body op is refused (E_INVALID_ARGS): delete the group handle.cad_batch {atomic:true}opts into all-or-nothing on the same primitive: on the first failing op (or the clock running out) every op the batch appended is removed again and the refusal carriesdetails.rolledBack(newest first) with every$handlenull;atomicimpliesstopOnError. Non-atomic batches are unchanged. Document op ids (box_3) are minted past any id the list still holds, so a delete followed by a new op never collides.cad_design_review— automated design-review packet. One call runs the whole review of one part: STEP import (or a session body), topology inventory,ops/check.jstier-1 validity, mass properties, DFM (tier-2) and standard views with vector HLR on an ISO/ASME sheet. Emits a versioned JSON document (cadsolver://schema/design-review,kind: cadsolver.design-review, v1) plus a self-contained printable HTML report rendered from it, and a drawing-sheet SVG. Adds nothing to the kernel — every module it drives is an existing entry point.
Two behaviours a caller may key off: stage isolation — a stage that throws becomes a fail record plus an error-severity finding and the remaining stages still run, so the tool returns a packet where six separate calls would have returned an error; and the verdict roll-up — blocked on any error-severity finding or any failed required stage, advisory on warnings only, otherwise clean. Findings from the two ops/check.js tiers are deduplicated, so summary counts distinct defects rather than mentions. A non-watertight body reports watertight: false with no volume/area/ centroid/inertia, matching cad_mass_properties' existing refusal.
Refusals (nothing written): E_INVALID_ARGS for both or neither of file/solid and for an out-of-range bodyIndex, E_IO for an unreadable or empty file, E_GEOMETRY for a STEP that parses but holds no solid.
NativeCAD 0.3.x
v0.3.8 “Chung”
Re-staged kernel from master at HEAD (previously staged 57fe9ceab255). Six commits landed after the 0.3.7 bundle was built and were not in the shipped artifact. cad_server_info's server.stagedKernel.gitSha reports the new sha.
Changed
- Exact analytic normals in
cad_mesh_payload. The encoder now substitutes the true surface normal (sphere / cylinder / cone / torus / plane) for the area-weighted facet normal. Measured oncad_spherer=25: vertex normals went from 0.62° mean / 3.86° worst off the analytic normal to float32 noise, and duplicated periodic-seam vertices from 7.73° apart to agreement. The seam stripe and the diamond herringbone a viewer used to draw on curved faces were both artifacts of the old path, not of the geometry. - Ruled faces triangulate in the metric first. Cylinder walls carried roughly 6x the triangles a ruled mesh needs; interior nodes are now dropped after triangulating, −54.9% triangles.
- Parity matrix regenerated. 87 capabilities, counts unchanged; the stale matrix had been blocking the post-commit site sync.
Note for callers keying off refusals
cadsb-122 §3 was inverted in this wave: a rim pick now blends (rim completion), with locality re-pinned under propagate:false. A caller asserting the old refusal text will see a success instead.
v0.3.7 “Bychkov”
Re-staged kernel from master 57fe9ceab255 (previously staged c9f2f618) — the blend/CFD/PMI lane and an earlier change landed since the last stage and were not yet in the shipped/staged artifact. cad_server_info's server.stagedKernel.gitSha reports 57fe9ceab2551f193638f3004a0888bacc0cb5a5.
New capability
- Cone/cylinder rim blends (new file
ops/blendConeRim.js).cad_fillet_edgeson a circular rim whose two support faces are CONICAL_SURFACE + CYLINDRICAL_SURFACE (a drill-point rim — the coaxial caseblendCircularRim's plane/cylinder logic does not cover) now builds an exact toroidal-band blend when both supports share the rim's axis. Two new named refusals,conical-seamandcurved-pair-channel, replace the old generic curved-support message on the ctc_01 sample's non-blendable conical edges (a single 118° drill-point cone's own seam is not a corner — no radius will ever blend it). - Channel/canal-surface blends (new file
ops/blendChannel.js). A first cut at rolling-ball blends over an elliptical spine, producing a B_SPLINE_SURFACE blend face for curved-pair edges that previously refused outright (4 of 6 curved-pair edges on the reference part now build watertight). - Oblique blend runout and corner ends.
planObliqueEndplus a relaxed side-face test lets a blend run out onto a non-perpendicular cap; independent-edge blends now handle oblique corner ends the same way. - Oblique-cap chamfer runout.
cad_chamfer_edgesrunout onto an oblique cap plane now builds watertight instead of refusing (chamfer-no-perpendicular-cap). - Rim-completion expansion (restored after a concurrent-commit base conflict by). A single selected arc on a partial rim now expands to the full rim via a bounded vertex-walk instead of refusing as a
partial-rimpick. - Blend-onto-blend. Provenance-exact band detection lets a second blend land on a face a prior blend produced, with a true-cause refusal (
.blendOntoBlend) when it can't. - PMI persistence across executor rebuilds. The presentation graph (PMI) now survives a graft-triggered executor rebuild instead of reverting to a stale snapshot.
- CFD wind-tunnel setup fix. Setup mode now delegates to
toggleCfdSetupMode()instead of proxy clicks; a previously-uninvoked preview UI now boots. - Web/mobile UI changes (File/Solver ribbons, mobile layout) landed in the same window but do not change this package's kernel or tool surface — see their own tickets.
What did NOT change
- The MCP tool surface (names, schemas) is unchanged; this release changes what the kernel underneath can do, not the surface itself.
- Every call that succeeded under the previously-staged kernel is expected to still succeed — this release is additive (new supported rim/channel families) or narrows a refusal's wording, not a behavior removal.
Verification
packages/mcp/test/run.js(43 files, 1444+ assertions) — green, with one known unrelated pre-existing gap:test/parity.test.js'sb3-sequential-adjacent-filletstaxonomy case refuses for the right outcome but a mismatched reason string (a kernel refusal-wording gap, not a staging regression — out of scope for this release ticket).packages/mcp/test/e2e/scenario-f-blend-smoke.test.js(unmodified) — 27/27 assertions, staged kernel SHA == master HEAD.packages/mcp/test/e2e/scenario-g-conerim-smoke.test.js(new) — 20/20 assertions: 4/4 CONICAL+CYLINDRICAL rim edges on the NIST ctc_01 sample blend watertight throughcad_import→cad_body_topology→cad_fillet_edgesagainst the staged/packaged kernel, provingops/blendConeRim.jsreaches the shipped artifact (not just the kernel sources).tools/parity-web/gate.js— exit 0, 87 capabilities tracked, every web gap named against a ticket.packages/mcp/plugin/build.js— builtcadsolver-mcp-0.3.7.mcpb(4546 KiB, 2276 entries); staged kernel verified in the archive (97 files, tree1481df324d70…, git57fe9ceab255).
v0.3.5 “Blomstedt”
Re-staged kernel from master bbe6d08 (previously staged 1d207a35, 2026-07-31) — every blend fix landed since is now live in the shipped/staged artifact. cad_server_info's server.stagedKernel.gitSha reports bbe6d0845bee364e280d09aa2c40910d47b14c96.
Six sprints of blend work were sitting un-staged behind the 0.3.4 install. None of these change a successful call's output — they change which calls that used to be refused now succeed, and in one case, which calls that used to silently produce a non-manifold body now either blend correctly or refuse honestly instead.
Refusals that are now successes
- Anti-parallel-axis circular rims.
cad_fillet_edgeson a circular hole/boss rim whose axis opposes the supporting plane's outward normal previously either mis-wound the result or refused outright.blendCircularRimnow derivessame_senseper rim edge instead of hard-coding.T., so both axis orientations blend to a watertight, correctly-wound, mirror-symmetric solid. Raised the coverage matrix's whole-rim floor 12 → 18 passes with zero anti-parallel refusals. - Rim seam duplication — a silent correctness bug, not a refusal (root cause).
blendCircularRimemitted each seam edge of a cylindrical rim twice (once per half-face), producing a shell that passed every existing mesh-based watertightness check (the tessellator welds by position) but was topologically non-manifold — every edge used by four faces instead of two. Fixed by emitting each seam once with all moves applied. Any downstream tool that consumes STEP topology directly (rather than a tessellated mesh) —cad_body_topology, a re-import, a round-trip through another CAD system — now sees a correct shell instead of a silently broken one. faceRing family 3/13 → 16/0 in the coverage matrix; overall handled rate 72.2% → 76.1%. - Partitioned mixed-body blends.
- Rim seam dedup, second pass (follow-through).
- Setback vertex blends and blend propagate now succeed on selections that previously refused.
Refusals that now name themselves correctly (still refusals — reclassify, don't retry)
- Six anti-parallel-axis rims that cannot be resolved by the same_sense fix above (no perpendicular cap to run out into) now refuse by name —
error.messagenames the specific geometric gap — rather than mis-winding a band silently. This is a new, narrower refusal reason, not a widening of an old one: a caller that pattern-matched the old generic message should re-check againstcad_server_info'serrorCodessection.
New capability, not a refusal→success change
- Overflow policy and variable-radius / laws-DSL blends —
radiusmay now be alawexpression (r(t)); seecad_fillet_edges' schema. - Conforming Delaunay volume meshing and gift-wrap cavity fallback — not blend-related, but landed in the same re-stage window; both change meshing/cavity behavior on bodies that previously refused or produced degenerate output.
What did NOT change
- Every call that succeeded under kernel
1d207a35still succeeds and still returns byte-identical geometry — the gate (tests/test_cadsf-377.js) pins "existing successes stay exact", and the "rate does not go backwards" floors are asserted, not just measured. - The MCP tool surface (names, schemas) is unchanged; this release changes what the kernel underneath the surface can do, not the surface itself.
Verification
tests/test_cadsf-377.js— the gate, run against the re-staged kernel: manifold check on the synthetic rim, the NIST rim, and a bounded slice of the full selection matrix.tools/blend_matrix.js --file nist_ctc_01_asme1_ap242-e1.stp --gate— the full 967-case matrix; zero UNCLASSIFIED refusals.packages/mcp/test/e2e/scenario-f-blend-smoke.test.js(new) — the same class of case, driven through the actual MCP tool surface (cad_import→cad_body_topology→cad_fillet_edges) rather than the kernel's internal API, so this proves the staged/shipped artifact behaves this way, not just the kernel sources.
v0.3.4 “Ashkenazy”
Staged kernel 1d207a35. See staleness gate: kernel/BUILD_INFO.json stamp + build-time verification that the archive's stamp matches the run that staged it.