Four mechanical binding edits; the bound Python surface is unchanged
(sigdiff compared the 7.9.3.1 dump against 8.0.1.1's in both directions
and found no class, member or constructor arity moved), so the app needs
no change.
- NCollection_Utf8String is gone; NCollection_String is the same UTF-8
type. Python name kept, since the app imports it.
- Standard_Failure lost its Standard_Transient RTTI when it moved to
deriving std::exception; ExceptionType() replaced DynamicType()->Name()
and returns the same class names the exception dispatch keys on.
- FindKey gained a size_t overload beside the int one, so the plain
member pointer is ambiguous; overload_cast picks the int form, which
keeps the negative-index guard.
- StdPrs moved from TKService to TKV3d, so CMakeLists links both.
The byte-identity fixtures did NOT retire, contrary to the watchlist:
all six round-trip to the same digests under the new kernel, as do the
measurement, history and mesh-count blocks, so tests/data is untouched
and the app's content-addressed BREP payloads stay valid.
Gates: binding suite 83 passed, 138/138 symbols, sigdiff clean both
directions, ASAN clean, app suite 1798 passed / 1 skipped (unchanged),
sweep over 4486 parts with anchors_digest and every status unchanged,
-m perf 16% faster on the 32-feature chain.
The wheel is cp312-abi3, so it loads on 3.13 unchanged — the pin that has to
move is the interpreter the parity venv is built with, since its whole point is
resembling what the image ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDYRXGB8tW4NE7b91g7Kdb
- GIL policy gains the XSTEP exception (STEP/IGES hold it; RWStl releases).
- A new rule, learned three times the hard way: an unregistered type cannot be
a default argument, because nanobind converts defaults at binding time and
the failure is a bare std::bad_cast at import with no file or line.
- A section on matching upstream: what sigdiff exists for, why static analysis
cannot see instance methods, and why the app's suite cannot gate a single
increment.
- An OCCT 8.0 watchlist, so the 10E bump is a known quantity.
- S5 marked settled: no CSF_* resources needed in the wheel.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
Binds the modeling core the app builds every feature out of — 93 of its 138
symbols now resolve, up from 34. New modules: GeomAbs, Geom2d, Geom,
TCollection, TColgp, TColStd, GProp, BRepGProp, Bnd, BRepBndLib, Adaptor3d,
BRepAdaptor, GeomLProp, GeomAPI, BRepBuilderAPI, BOPAlgo, BRepAlgoAPI,
BRepPrimAPI, GC, BRepMesh; gp and BRep_Tool completed.
Three structural decisions:
- BRepBuilderAPI_MakeShape carries Build/Shape/Generated/Modified/IsDeleted for
every maker in the binding, so the booleans, the primitives and (later) the
fillet builders all answer the app's duck-typed provenance layer through
ordinary virtual dispatch. History lists come back copied, so they outlive
the builder.
- The executing two-argument BRepAlgoAPI constructors stay unbound; operands go
in through SetArguments/SetTools. Section keeps Init1/Init2, which are plain
setters. BOPAlgo moved up from Inc 2 — SetGlue needs its enum.
- Adaptor3d is registered although the app never imports it: every method it
calls on BRepAdaptor_Curve/Surface is a virtual declared there, so binding
them once on the bases leaves mod_BRepAdaptor.cpp with just constructors.
Gate: tests/test_inc1_modeling.py against reference values gen_fixtures.py now
records from the stock wheel — measurements, per-face area/centroid in map
order, mesh counts, and the boolean history map compared exactly, since that is
the substrate the app's topological naming is built on.
tools/sigdiff.py compares our bound surface against stock's, because a wrong
nb::init<> is silent: MakePrism's five-argument form bound OCCT's semi-infinite
gp_Dir overload (gp_Dir converts from gp_Vec), producing a valid solid of the
wrong shape with the flags shifted along. The fixture digest caught it; sigdiff
finds the class of bug directly, and now reports only one deliberate deviation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
- inventory.py --methods: report the instance methods the app calls per class,
plus chained calls that constrain a return type. --check says which classes
to bind; this says what to bind on them, which is what writing ~38 module
TUs needs.
- Drop StlAPI from the inventory: StlAPI_Writer has no app call site (the only
use was a test fixture, now on the app's own STL writer). 138 symbols / 47
modules.
- adding-symbols.md: scope the executing-constructor ban to the BRepAlgoAPI
booleans, which are the only classes with a deferred Set*/Build form —
BRepMesh_IncrementalMesh, GeomAPI_*, BRepCheck_Analyzer and friends compute
in their constructor by design and bind as stock. Replace the per-increment
app-test guidance: backend/tests/conftest.py imports n3xd.main, so no app
test can collect until the last module is bound. Increments gate on
stock-recorded fixtures here; the app suite is the Inc 4 gate.
- parity_venv.sh: run the ocp suite in the swapped venv (it imports only
OCP/n3xd_ocp, so it works throughout).
- Fix a stale macro name in occt_handle.h (ocp_new, not OCP_TRANSIENT_NEW) and
drop the unused ocp_transient_class helper.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
Builds n3xd-ocp end to end and publishes 7.9.3.1.dev1 to the Gitea registry,
where it installs anonymously and passes its suite.
- occt/Dockerfile: OCCT 7.9.3 compiled once into a manylinux_2_28 builder
image (base digest + tarball sha256 pinned), Draw/VTK/Tk/Xlib/OpenGL off,
FreeType on, -O2 without fast-math or march=native. A final layer asserts
TKService/TKV3d exist with no libGL/libX11 DT_NEEDED, which is what lets the
app image drop libgl1/libx11-6. Mounted into, never built FROM.
- scikit-build-core + nanobind STABLE_ABI -> one cp312-abi3 extension that
registers every OCP.* submodule via PyImport_AddModule, so `import
OCP.TopoDS` needs no shim and cls.__module__ is right. Version <occt>.N is
asserted against the OCCT found, keeping occt_version() truthful.
- occt_handle.h: type caster for opencascade::handle<T> over OCCT's intrusive
refcount. Wrappers are non-owning instances holding exactly one handle in
their keep-alive list, reusing an existing wrapper so identity survives a
round trip. Transient constructors go through ocp_new (never nb::init<>,
which would let OCCT delete nanobind's storage); the caster refuses a
refcount-0 object rather than corrupt the heap. Verified under ASAN with no
memory-safety errors, plus an RSS bound over 50k create/destroy cycles.
- Sub-shapes are returned by value everywhere, making the TShape lifetime class
that segfaulted a process-global face memo unrepresentable.
- Standard_Failure derives RuntimeError, with ~20 concrete types dispatched on
the dynamic OCCT type (cad_pool marshals failures home by type name).
- Inc 0 surface: gp subset, TopAbs, TopoDS (+ downcasts), TopExp, TopLoc,
TopTools, BRep, BinTools, Poly, Standard. 34 of the app's 139 symbols.
- n3xd_ocp: additive APIs kept out of the OCP namespace so parity testing stays
meaningful. bintools (shape <-> bytes, GIL-free, byte-identical) and _debug.
Two findings worth the record, both verified against the stock wheel rather
than assumed: upstream binds __hash__ but leaves __eq__ at identity, which is
exactly what geom_memo.py's hash-bucket + IsSame scan is built around, so we
match it instead of "fixing" it; and BinTools can release the GIL after all, by
slurping the file-like object instead of bridging a streambuf that would call
back into Python.
Gate: BREP round-trips are byte-identical to cadquery-ocp-novtk across six
fixtures (the generator asserts stock idempotency first). That matters beyond
IPC — derive.py content-addresses BREP payloads by sha256 and stores the ref.