OpenSKP OpenSKP
Release Notes

Every release, in full detail

What shipped, what was fixed, and why — across Python, TypeScript, .NET, Dart, and C++. This page is the complete record; nothing here is trimmed for a changelog summary.

1.3.0 Python + C++ — GitHub-only pre-releases, not on PyPI or a numbered C++ release yet
Not published to PyPI

This is a real, fully tested, tagged GitHub Release (538 tests passing, lint clean, package builds cleanly) — but it's deliberately not on PyPI yet, so pip install openskp won't find it. Install it by pinning the tag directly:

pip install "openskp[fragments] @ git+https://github.com/iamahsanmehmood/openskp.git@preview-python-v1.3.2#subdirectory=packages/python"

It folds into a proper PyPI 1.3.0 release once TypeScript/.NET/Dart catch up to parity on the items below — tracked on the public roadmap. C++ has now shipped a matching port of most of this release — see the dedicated section below.

Headlined by a brand-new capability: direct SketchUp → ThatOpen Fragments (.frag) export, no IFC intermediate — now shipped in both Python and C++, with C++ measured roughly 5-9x faster end to end on the same real files. Also four real correctness fixes to IFC export (both languages), a triangulation engine swap that's both faster and more correct (Python), fixes for a large real file (359 MB, 95,363 definitions) that previously exhausted all available memory (Python), and a fix so every attribute dictionary an instance carries actually reaches GLB/JSON metadata export instead of only the derived .ifc (both languages). See the language parity matrix for the full, current picture.

Added — Direct SketchUp → Fragments (.frag) export

  • New openskp.export.fragments module (to_fragments(scene, *, raw=False) / export(scene, output_path, *, raw=False)) writes ThatOpen's public Fragments/FlatBuffers format straight from an InstancedScene: real nested spatial hierarchy matching the source file's own component nesting, IfcImporter-compatible display names, per-item GUIDs (the source file's real SketchUp instance GUID on VFF/2021+ files, a stable synthetic one on legacy files), and non-unit scale/mirrored instances baked correctly into geometry. New optional dependency: pip install openskp[fragments]. Verified against the real @thatopen/fragments runtime, not just this project's own reader, including a production-scale real file (12,338 items).
Known gaps in Fragments export, stated plainly

C++ has this too now (see the dedicated section below) — a community TypeScript port is open (PR #276) but its required lint check is currently failing, not mergeable as-is; .NET/Dart have no work started. The Fragments format itself has no native visibility field — per-layer hidden state is carried via a Model.metadata JSON sidecar, a convention this project defined, not part of the public ThatOpen schema. Model.guid (the single model-level identifier) is still an unpopulated placeholder.

Changed — face triangulation: Shapely replaced with earcut

  • A convex, hole-free face now triangulates via a direct fan (zero fidelity change, 10–44% faster on real fixtures); every other face now triangulates via mapbox_earcut instead of Shapely's Delaunay-plus-centroid-containment-filter approach (42–73% faster on top of that). Also a real correctness fix, not just speed: Shapely's centroid-containment filter was found under-covering a real concave 27-vertex face by roughly 10.5% of its area, confirmed against an independent shoelace-formula ground truth.

Fixed — 4 correctness issues in IFC export

  • Units and axis convention — coordinates were written inch-scaled but labeled as millimetres (~25.4× off), and the axis convention reused glTF's Y-up values directly instead of converting to IFC/SketchUp's Z-up — a real export came out rotated ~90° and mirrored.
  • Real instance names and layer visibility — elements were previously named after internal mesh-lookup keys instead of the real instance name; switched to IfcPresentationLayerWithStyle so layer on/off state actually carries through. Legacy files now get correct on/off state; VFF (2021+) files still always report visible in this specific export path — tracked as an open follow-up.
  • Plugin attribute dictionaries surfaced as element names and IFC properties — every attribute dictionary an instance carries (not just dynamic_attributes) is now extracted, with name/label/code used for the display name when present, and each dictionary becomes its own Pset_<dict-name> in IFC output. Targets real steel-detailing plugin data that previously had no way to reach the exported model at all.
  • Opt-in full-path keyword classification — classify_using_full_path (default False) falls back to matching the full ancestor-path string when a name/layer match both miss.

Added

  • Read the real per-layer hidden flag from VFF (2021+) files (a new 8E3C tag) — previously every VFF layer always read as visible.
  • layer_hidden now included in both GLB exporters' metadata sidecars.
  • Read pages/scenes for legacy (pre-2021) files, narrow scope — only "nothing captured" or "hidden layers only" pages are supported; a fuller capture (camera, style, shadows) is silently skipped rather than guessed at.
  • Write section planes (add_section_plane(point, normal)) and construction lines/points (add_construction_line/add_construction_point), both ground-truthed against real SketchUp saves.
  • Face-me components in the writer — always_faces_camera/shadows_face_sun on add_component_definition.

Fixed

  • A real 359 MB / 95,363-definition production file that previously exhausted all available memory on a 40 GB machine now completes in ~773s at a peak of ~12.5 GB RSS. Two independent bugs: a container-unwrapping gap that materialized an entire dominant top-level record's subtree before any of it could be released, and an O(V²) linear nearest-vertex scan in triangulation replaced with an O(1) lookup. Fixes #264.
  • Persistent IDs now run in one sequence across every writer section instead of restarting at 1 — real SketchUp would load such a file fine, then SUModelSaveToFile failed on any non-trivial re-save.
  • front_uv/back_uv pins now land where SketchUp actually draws them, on any face and at any applied size — the writer's basis and scale were both wrong for anything other than an axis-aligned face at 1 inch/tile. Also fixes the reader's vertical-tolerance and downward-face basis.

⚙️ C++: ported most of this release, plus closed its own standing gaps

Not a numbered cpp-v1.3.0 release yet

Real, fully tested, tagged GitHub Releases — preview-cpp-v1.3.0 (216 tests passing) followed by preview-cpp-v1.3.1 (217 tests passing, both verified against a fresh extraction of the packaged archive, not just the working tree) — but not on a package registry, so find_package(OpenSkp CONFIG REQUIRED) against a normal install won't see it. Build it by checking out the tag directly and following the README's C++17/CMake Quick Start, or download the source archive from the release page.

  • Direct SketchUp → Fragments (.frag) export — openskp::to_fragments()/export_fragments(), a line-for-line port of Python's own implementation: real nested spatial hierarchy, per-item GUIDs, non-unit scale/mirrored instances baked into geometry. Verified against the real @thatopen/fragments runtime on two real production files (2,239 and 14,694 items). Roughly 5-9x faster end to end than the equivalent Python pipeline on the same real files — ~9.6s vs ~84s parse+bake on a 14 MB file, ~97s vs ~500s on a 173 MB file.
  • Multiple attribute dictionaries per entity — Instance::attribute_dictionaries/InstancedNode::attribute_dictionaries now expose every dictionary an instance carries by its own declared name, not just SketchUp's own dynamic_attributes. Fixes a real bug where a third-party plugin's own dictionary (e.g. a steel-detailing tool's own named dictionary) silently mixed into the wrong property bag.
  • The same 4 IFC export correctness fixes as Python — units/axis convention, real instance names + real layer on/off state via IfcPresentationLayerWithStyle, plugin attribute dictionaries as Pset_<dict-name> properties, opt-in full-path keyword classification.
  • Attribute dictionaries now reach GLB/JSON metadata export, not just IFC — every attribute dictionary an instance carries was already correctly resolved by build_scene(), but json_export.cpp never wrote it into instance_node_to_json/mesh_metadata_to_json output; only the derived .ifc ever saw it, as Pset_<dict-name>. Same fix shipped to Python (PR #299) — verified against real plugin data (steelframer-dict, 45 entries) in both languages.
  • Real per-layer-hidden flag reading from VFF (2021+) files (the 8E3C tag) — previously every VFF layer always read as visible.
  • model.layers now in source-file order, not alphabetical — matching the other four languages.
  • Legacy (pre-2021) pages/scenes reading, and construction lines/points reading (legacy files only) — mirroring Python's own narrow-scope implementations.
  • mesh_index[...].properties/.attribute_dictionaries now backfilled in the baked (build_scene()) path, matching every other language.
C++ known gaps, stated plainly

Attribute dictionary values still decode as strings only — no native Point3d/Vector3d/Length/nested-list support, matching C++'s existing string-only property handling elsewhere. The legacy pages/scenes reader hasn't been exercised against a real file that actually contains a scene (synthetic test only, plus real-file smoke testing). Construction points were verified byte-for-byte against a real fixture; construction lines weren't — no available fixture contains one. Full detail on the language parity matrix.

📘 TypeScript: writer memory fix (unreleased on npm)

ArchiveWriter now keeps the archive in a growable Uint8Array instead of a number[], and SkpBuilder.toBytes() assembles the file into one exact-size buffer instead of a second number[] plus a final copy. A number[] costs about 9 bytes of heap per file byte, so a 62 MB write peaked at ~2.1 GB of transient heap and could take a browser tab down; it now peaks at ~0.2 GB. Output is byte-identical, no public API change. Merged on main, not yet on npm.

Honest status: not every real old-format file parses yet

A 14-file real-world version sweep (the same project saved across SketchUp versions 3 through 2025) found 8/14 parse and convert cleanly; 6 fail with distinct legacy-format errors (versions 3, 4, 6, 7, 8, 2013, and 2019). The V7/V8/2013 failures share one root cause — a slot-collision bug in the legacy archive reader — narrowed down to inside one nested group's own recursive content, but not yet fixed. This was only investigated against the Python legacy MFC reader — each other language has its own independent parser. Tracked on the roadmap, not glossed over.

1.2.0 Full attribute dictionaries, group attributes, Image entities — all 5 languages

The writer's custom attribute dictionaries gain full type support in Python and a real multi-dictionary API; groups can carry attributes in all 5 languages for the first time; applied texture width and material opacity round-trip correctly everywhere; SketchUp Image entities, linear dimensions, leader text, and the SKP-to-code generator all reach parity across the language ports.

Added

  • Full attribute-dictionary support in the writer, Python. Every value type the reader already decoded — None, bool, int, float, str, Point3d/Vector3d, Length, Timestamp, and nestable lists — plus multiple named dictionaries per entity via attribute_dicts=[(name, entries), ...], Python-only for now.
  • Groups gain attributes, all 5 languages. add_group/add_group_instance get the same attributes/attribute_dict_name parameters add_instance already had, at single-dictionary parity across languages.
  • Applied texture width and material opacity, all 5 languages. A texture applied without explicit positioning now carries a real mapping in both axes, and materials round-trip transparency correctly.
  • SketchUp Image entity writer support (add_image), all 5 languages.
  • Writer support for linear dimensions and leader text, Python. VFF scene/page and dimension parsing ported to all 5 languages.
  • Instancing-preserving scene output, all 5 languages (build_instanced_scene()/to_instanced_glb()) and the SKP-to-code generator, all 5 languages.
  • 6 legacy MFC reader gaps closed, all 5 languages — textured colour-by-layer, dimension-linear structural trailer, forward-tolerant relationship/text-leader refs, Image entity reading, two attribute types, and a store-map-index bookkeeping rework.

Fixed

  • Six cross-language correctness bugs folded in from ongoing testing: CoEdge orientation inversion, a Dynamic Component attribute-lookup bug (compared class name instead of dict name), material transparency dropped from GLB export, XML entity decoding in material/style names, a TLV header-scan off-by-one, and a mesh-name cascading bug (#240) — each across the language ports where it applied.

Changed

  • Python now requires 3.10+ (3.9 dropped). Writer validates material/layer handles at call site, all 5 languages, raising a clear error on a mismatched handle instead of producing a silently broken file.
1.1.0 Write support, now in all 5 languages

OpenSKP can create new .skp files from scratch, not just parse existing ones — a genuine, from-scratch binary writer for the legacy MFC CArchive format (SketchUp 2013–2020), built by inverting the existing reader's own decoding logic rather than wrapping any SDK. This landed in Python first; this release ports the identical feature set — geometry, materials, layers, component definitions and groups (including nesting), circular/arc curves, freeform polylines, faces with holes, auto-triangulation, custom attributes, and the open_existing()/edit path — to TypeScript, .NET, Dart, and C++. TypeScript, .NET, and Dart are each verified byte-identical to Python's own already-SDK-validated output on the same input; C++ (no local compiler available in this project's environment) is verified structurally — every embedded byte constant cross-checked against Python's real values — plus its own full test suite passing in CI across GCC, Clang, and MSVC.

Entry point per language

Pythonopenskp.create() / openskp.open_existing()
TypeScriptcreate() / openExisting()
.NETSkpCreate.NewFile() / SkpEdit.OpenExisting()
Dartcreate() / openExisting()
C++openskp::create() / openskp::open_existing()

Added

  • All 5 languages — the writer. SkpBuilder assembles a legacy MFC CArchive-format .skp file byte-for-byte: solid-color and PNG/JPEG-textured materials, named layers, reusable component definitions with multiple positioned instances, groups (including nested definitions and nested group instances to any depth), per-instance rotation and visibility, explicit per-side texture positioning, custom key/value attribute dictionaries, genuine editable-by-radius circular/arc curves, freeform polyline curves, faces with one or more holes cut out, and non-planar auto-triangulation. No SketchUp SDK involved at import, build, or save time — it works by inverting each language's own reader logic against a small, disclosed, SDK-authored blank-document scaffold it splices new entities into.
  • All 5 languages — editing an existing file. open_existing() fully parses a source file with the project's own reader, then replays everything it understood — materials, layers, every component definition, all root-level geometry and instances — back through the writer's own API, producing a brand-new file more geometry can still be added to before saving. Returns a warnings list for anything that couldn't be faithfully reproduced, rather than silently dropping it.
  • TypeScript — toGLB(scene, { textures: true }). Embed the scene's texture images in the GLB export and point each textured material's baseColorTexture at them, opt-in since it can multiply the exported file's size. Also fixes material identity: materials were previously deduplicated by colour alone, silently merging two different textures that happened to average to the same RGB — the texture is now part of the dedup key too. Contributed by Thomaz Yuji Baba.

Fixed

  • TypeScript — the v20 filler-record count reader misidentified a count that was an exact multiple of 256 (its own low byte is 0x00, indistinguishable from padding to a byte-at-a-time scan), misaligning every read after it. Rewritten to probe whole u32s at 4-byte strides instead of inspecting individual bytes. Contributed by Marco Sumari, a follow-up to a review note on an earlier PR.
Explicitly out of scope for this pass

Declaring a group's geometry inline nested inside another definition's own body — this format has no mechanism for one definition's declaration to live inside another's, so a nested group's geometry has to be built separately first. Writing the modern VFF (2021+) container — every language's writer only ever produces the legacy MFC format, the same restriction open_existing() places on its own source file. Converting other formats into .skp (glTF/IFC/OBJ → SketchUp) is a planned future direction built on this writer, not yet under way.

1.0.0 First stable release — all 5 languages

All five language ports (Python, TypeScript, Dart, C# / .NET, C++) now carry full feature parity: parsing (geometry, materials, layers, dynamic properties, metadata) and native, dependency-light export to GLB, JSON, Wavefront OBJ/MTL, STL, PLY, DXF (3DFACE and AutoCAD Polyface Mesh), and IFC4 (BIM) — verified against real-world .skp fixtures and, for DXF specifically, against real desktop AutoCAD rather than lenient readers alone.

Added

  • All 5 languages — native Wavefront OBJ exporter. to_obj/toOBJ/toObj/ToObj, plus a file-writing counterpart in each language. InvariantCulture/classic-C-locale formatting is enforced everywhere to guarantee dot decimal separators regardless of the host OS locale.
  • All 5 languages — native STL exporter, both ASCII (.stl) and little-endian binary (_bin.stl), with an optional scale multiplier (1.0 for metres, 1000.0 for mm — matching what slicers like Cura, PrusaSlicer, and Bambu Studio expect). Verified byte-identical output (9,368,084 bytes, 187,360 triangles) across languages on the same real fixture.
  • All 5 languages — native PLY exporter (Polygon File Format), both ASCII and little-endian binary, carrying vertex positions, normals, UV coordinates, and RGBA vertex colors. C++'s binary writer uses architecture-independent bitwise shifts rather than relying on host endianness. Verified byte-identical output (9,316,015 bytes, 187,360 triangles) across languages on the same real fixture.
  • All 5 languages — rich Wavefront OBJ/MTL extension. A companion .mtl material-library writer emitting newmtl/Ka/Kd/Ks/Ns/d/illum records plus map_Kd texture references, with the OBJ writer gaining an optional MTL-filename parameter to link the two via mtllib. The OBJ writer itself gained vt (UV) and vn (normal) records, upgrading it from a positions-only debug dump to a properly textured/shaded mesh format.
  • All 5 languages — native 3D DXF exporter, targeting AutoCAD R2000 (AC1015) compliance — full HEADER/CLASSES/TABLES/BLOCKS/OBJECTS scaffold, LAYER table entries with ACI color codes, and entity output in either 3DFACE mode or AutoCAD Polyface Mesh mode. Polyface is now the default mode (previously 3dface), since it's the form AutoCAD itself generates for triangulated meshes.
  • All 5 languages — native IFC4 (BIM) exporter — ISO-10303-21 STEP ASCII output with a full IfcProject → IfcSite → IfcBuilding → IfcBuildingStorey → IfcProduct spatial hierarchy, IfcTriangulatedFaceSet tessellated geometry, best-effort element classification (walls/doors/windows/slabs/columns/beams/roofs), dynamic properties, material colors, and layer preservation. Validated with IfcOpenShell against two real-world files, including a 26.94 MB / 101,692-property export.
  • Web viewer — the single "Export GLB" button is now an "Export Model" dropdown covering every format the library supports: JSON, IFC4, DXF (both modes), PLY, STL, GLB, and OBJ.
The deepest fix in this release: DXF files that lenient readers accepted but real AutoCAD rejected

The zero-dependency DXF writer produced syntactically valid DXF that ezdxf.readfile() — and even ezdxf's own .audit() — accepted without complaint, but desktop AutoCAD rejected outright: first with "Invalid or incomplete DXF input", later with "Did not receive PlotStyleName" once the more obvious problems were fixed. ezdxf is too lenient to catch what AutoCAD enforces internally, and there's no public, comprehensive list of those rules — so this was tracked down using real desktop AutoCAD as the only reliable oracle, then cross-checked against what real ezdxf itself emits for equivalent structures.

Five distinct root causes, byte-verified against a real ezdxf-generated file confirmed to open cleanly in AutoCAD:

  1. An incomplete HEADER/CLASSES/TABLES/BLOCKS/OBJECTS scaffold — a near-empty CLASSES table, a fabricated VPORT record, and a stripped OBJECTS dictionary tree missing MATERIAL/MLINESTYLE/MLEADERSTYLE records and a second LAYOUT record — all boilerplate AutoCAD requires but lenient readers never check.
  2. Every dynamically-generated LAYER record was missing group 370 (lineweight) and 390 (PlotStyleName handle); AutoCAD rejects the whole TABLES section without them once a drawing uses a Named plot style table. Also dropped group 420 (24-bit true color), an R2004+ field real ezdxf never emits for R2000/AC1015 — ACI (62) is the only color mechanism R2000 supports.
  3. Polyface POLYLINE/VERTEX/SEQEND structure had three mismatches against real ezdxf's own polyface output: face-record VERTEX entries were missing color (62) and dummy 0/0/0 coordinates while carrying a subclass marker they shouldn't have, and SEQEND's owner (330) pointed at Model_Space instead of its parent POLYLINE's own handle. This is exactly why 3dface mode opened fine while polyface mode was rejected, despite sharing the same scaffold.
  4. TypeScript, Dart, C#, and C++ never substituted the $HANDSEED placeholder — every file these four exporters ever produced shipped the literal text __HANDSEED__ instead of a real hex value.
  5. Unbounded layer names could exceed AutoCAD's documented limits; added a defensive 255-character cap with a collision-safe hash suffix on truncation.

Dynamic handles now start at 0x691, matching the reference file's own first entity handle, keeping every handle in an export globally unique. Both 3dface and polyface output confirmed opening cleanly in real desktop AutoCAD after this fix.

Fixed

  • Python — export.dxf.export() didn't set newline='' when opening the output file on Windows, so open()'s universal-newline translation turned every \r\n the writer emitted into \r\r\n (double carriage returns), which CAD viewers rejected.
  • TypeScript — toGLB() never wrote a TEXCOORD_0 accessor despite GlbPrimitive.uvs existing on its data model — real UV data was computed but the actual exported .glb bytes never carried it. No test coverage existed for toGLB() before now, which is how this went unnoticed.
  • C++ — Face.uv_transform/uv_transform_back were never populated for any legacy MFC (SketchUp 2013–2020) file. Root cause: CFace's attribute container was being read (correctly advancing the archive cursor) and then discarded, never linked back to the face. Verified against a real fixture: the count of faces with a real uv_transform now matches Python's independently-verified count exactly (32) — previously always 0.
  • Python — export.glb.export() crashed on any file with a textured material: the metadata JSON sidecar tried to embed each material's raw texture image bytes directly, which was never JSON-serializable.
  • Python, TypeScript, Dart, C++ — Material.color's alpha channel was silently discarded when parsing legacy MFC files; each language read the real 4-byte RGBA record but only kept the first three bytes. .NET already read it correctly; the other four now match. Verified empirically across 2,060 materials in 13 real production files that this byte is always 255 in practice, but reading the real value is more correct than assuming a constant.

Changed Breaking

  • Python — SkpModel.definitions no longer contains an entry keyed by the string "ROOT". The implicit top-level model now has its own dedicated SkpModel.root field, matching TypeScript/.NET/Dart/C++'s root/Root exactly. Any code iterating model.definitions.values() to sum geometry across the whole file needs to also include model.root now. len(model.definitions) is one lower than before.

Removed

  • Python — SkpModel.scene_hierarchy and SkpModel.mesh_index: dead fields that parse() never populated (always empty). The real, populated versions of both live on the separate Scene class returned by build_scene().
0.3.1 TypeScript only

Fixed

  • TypeScript — the published npm README was still showing the pre-implementation placeholder page ("Under active development... coming soon", a planned-features list, no working examples), unchanged since before the TypeScript port was actually written. Rewritten to describe the real, working package: accurate parse()/buildScene()/toGLB()/toJSON() quick start, observability options, and the known large-file memory limitation. Python, .NET, and Dart's READMEs were audited at the same time and found already accurate.
0.3.0 C++ only

Added

  • C++ package — new independent C++17 port covering both modern VFF/ZIP (2021+) and legacy MFC (2013–2020) containers, at parity with the other four languages: geometry, components, layers, materials/textures, styles, dynamic properties, image entities, parse()/build_scene(), and observability hooks. Installable CMake package with static and shared library builds, cross-platform CI (Linux/macOS/Windows, GCC/Clang/MSVC). Contributed by Thomas Loockx.
0.3.0 Python, TypeScript, .NET, Dart

Added

  • .NET and Dart packages built from scratch, at full parity with Python/TypeScript: geometry, components, layers, materials/textures, styles, dynamic properties, image entities, plus full legacy MFC support.
  • All four languages — opt-in scene baking (build_scene()/buildScene()/BuildScene()): resolves the entire placed instance tree to world-space, triangulates every face, and groups results into GLB-ready mesh primitives. Kept deliberately separate from the light default parse().
  • All four languages — memory fix for large real-world files. Files with 100,000+ component definitions previously required materializing the entire file's tree in memory before extraction could begin; peak memory now scales with the largest single top-level record instead of the whole file. Verified against real production files up to 620 MB. .NET additionally needed a chunked buffer plus widening TLV offsets from int to long, since the CLR's array/MemoryStream types have a hard ~2.1 GB ceiling.
  • All four languages — observability: opt-in progress reporting and structured, location-carrying parse errors, silent by default.
  • Python — material IDs, instance material inheritance, texture extraction, colourized materials, per-face texture mapping (uv_transform), back-side face materials, per-edge display flags, rendering styles, "always faces camera" billboard behavior, and image entities.

Fixed

  • Python — entity names now decode as UTF-8 instead of ASCII-with-ignore; dropping non-ASCII bytes silently corrupted accented names and broke the material-name join between the TLV stream and XML material files.

Changed Breaking

  • Python — Material.transparency now correctly exposes opacity as 1 - trans rather than the raw transparency value. Most materials move from reading 50% transparent to fully opaque; genuinely translucent ones invert. Audited against SketchUp's own library materials.
0.2.0 All languages

Added

  • SketchUp 2025 file support
  • Materials rendering support
  • Older SketchUp version fixes
0.1.0 First public release

Added

  • Python package — parsing SketchUp 2021+ (VFF) files: geometry, component hierarchy, layers/tags, materials, dynamic properties; export to GLB, Wavefront OBJ, and JSON; CLI entry point.
  • TypeScript — type definitions and stubs (implementation to follow).
  • Dart — placeholder package (planned for a future release).
  • Initial documentation: reverse-engineered binary format spec, architecture overview, cross-platform API design.
  • CI/CD: GitHub Actions test matrix for Python (3.9–3.12 × Linux/Windows/macOS) and TypeScript, plus a PyPI release workflow.