Contributing
Before changing code
- Read
AGENTS.md. - Keep IFCX independent of
openbimrs/ifcinternals, and never makeifcdepend on this repository. - Name the IFCX draft revision that a capability targets.
- Do not commit buildingSMART schemas or samples without verified redistribution rights.
Verification
Run the complete gate from the repository root:
./scripts/gate.shThe command must exit successfully. It runs two sections, which CI runs as parallel jobs: rust (format, tests, clippy, rustdoc, the generated docs and the docs site build, packaging) and bindings (the npm package built with the pinned wasm-bindgen CLI and tested under Node; the Python wheel built with maturin, installed into a uv venv, and tested). Run one with ./scripts/gate.sh rust. The bindings need wasm-bindgen-cli 0.2.128, Node, uv and maturin on PATH; a missing tool fails the gate and names what to install. IFCX_SKIP_JS=1 or IFCX_SKIP_PYTHON=1 skips a suite locally with a warning; CI never sets them. Update README, rustdoc, capabilities, and CHANGELOG together when a user-visible contract changes.
Documentation
The documentation site, https://openbimrs.github.io/ifcx/, is built with VitePress from docs/, with the rustdoc of every crate under /api/rustdoc/ and the viewer under /viewer/. Everything the site states that can be derived from the source is generated by the xtask crate, and the gate fails when a generated page or region is out of date:
cargo run -p xtask -- docs # regenerate
cargo run -p xtask -- docs --check # what the gate runsEdit the source, never the generated output:
| On the site | Edit | Generated into |
|---|---|---|
| Crate reference pages | the crate's Cargo.toml, README.md, CHANGELOG.md, crate docs and public API | docs/reference/ |
| JavaScript and Python API tables | crates/openbim-ifcx-wasm/src/api.rs, js/fetch-imports.d.ts; crates/openbim-ifcx-py/python/openbim_ifcx/__init__.py | the binding reference pages |
| Changelog | the root and per-crate CHANGELOG.md files | docs/project/changelog.md |
| Contributing, Releasing | this file | docs/project/contributing.md, releasing.md |
| The viewer | demo/README.md | docs/guide/viewer.md |
| ADR index and sidebar | docs/adr/NNNN-*.md (# NNNN — Title, - **Status:**) | docs/adr/index.md, docs/.vitepress/data/adrs.json |
| Upstream evidence | scripts/upstream-drift/baseline.txt | docs/evidence.md |
| Install table, rustdoc list, README crate table | crate manifests and changelogs | <!-- NAME:BEGIN --> regions of docs/guide/install.md, docs/api/index.md, README.md |
Prose outside the <!-- NAME:BEGIN --> / <!-- NAME:END --> markers is written by hand, as are the home page, the guides, docs/capabilities.md and the ADRs. The code on the guide pages is imported from #region blocks of tests that the gate runs: crates/openbim-ifcx-geometry/tests/guide.rs, crates/openbim-ifcx-wasm/tests/js/guide.mjs and crates/openbim-ifcx-py/tests/python/test_guide.py. Change the code there. Every crate keeps its own README.md and CHANGELOG.md; the docs check fails without them.
To preview the site, install the pinned toolchain (package.json, package-lock.json) once, then:
npm ci
npm run docs:dev # live preview of docs/ at http://localhost:5173/ifcx/
npm run docs:build # what the gate runs when node_modules existsThe preview has neither rustdoc nor the viewer; .github/workflows/pages.yml adds both when it deploys main. Mermaid diagrams need accTitle and accDescr (docs/scripts/check-diagrams.mjs). Nothing from buildingSMART/IFC5-development may be published: scripts/check-leakage.py checks the source tree in the gate and the built site, against an upstream checkout, before every deployment.
Upstream drift
IFCX is an alpha draft. The evidence in docs/capabilities.md was recorded against a pinned buildingSMART/IFC5-development commit, and .github/workflows/upstream-drift.yml re-checks it against upstream's default branch every Monday at 04:23 UTC. It runs scripts/upstream-drift.sh, which fetches the ifcx.dev imports into an offline mirror and runs every opt-in upstream check: round trip, validation, composition, layer stacks, geometry decode, render scenes, composition parity with upstream's TypeScript, and the Khronos glTF validator. Each check's summary line (counts and failures, without timings) is compared with scripts/upstream-drift/baseline.txt.
When it finds drift, a failed check or a changed summary line, it opens an issue labelled upstream-drift, a sub-issue of #40. If one is already open it updates that issue and comments, so there is never more than one. The issue names the upstream commit and the failed checks, and links:
- the run and its
Upstream checksjob log, where each check is a collapsible group; - the
upstream-drift-reportartifact, withlogs/<check>.logper check,report.md,summary.txt, andbaseline.diff.
Start from baseline.diff and the failed check's log, then reproduce locally against the same commit:
git -C ../IFC5-development fetch && git -C ../IFC5-development checkout <commit>
IFCX_UPSTREAM_DIR=../IFC5-development ./scripts/upstream-drift.shThen either fix the crate, if upstream changed the draft or found a bug, or accept the new results: update the counts and the named revision in docs/capabilities.md, run ./scripts/upstream-drift.sh --update-baseline, and commit both. The first scheduled or dispatched run against the default branch that passes closes the issue. Known gaps are part of the baseline; for example, ifcx.dev does not serve ifc-mat/prop@v1.0.0.ifcx (upstream #124), so 4 examples have an unresolved import. If upstream fixes that, the baseline differs and the issue says so.
The run summary also lists the upstream branches (draft work such as post-alpha happens there) that moved, appeared, or disappeared since the last recorded run. A moved branch alone opens no issue. The record is the upstream-drift-state artifact (branch heads and the checked revision, kept 90 days) of the latest non-dry run on main.
Run it by hand from Actions -> Upstream drift -> Run workflow, or:
gh workflow run upstream-drift.yml -R openbimrs/ifcx --ref main
gh workflow run upstream-drift.yml -R openbimrs/ifcx --ref main \
-f upstream_ref=post-alpha -f dry_run=trueupstream_ref checks a branch, tag, or full commit SHA instead of the default branch. dry_run only writes the run summary: no issue is opened, updated, or closed, and no state is recorded.
Fuzzing
fuzz/ holds cargo-fuzz targets for the reader, composition, validation, and the geometry decoders. It is its own Cargo workspace with its own Cargo.lock, outside the root workspace and the MSRV gate, because libFuzzer needs a nightly toolchain; fuzz/rust-toolchain.toml pins the date.
| Target | Exercises |
|---|---|
read | IfcxFile::from_json_slice on arbitrary bytes; a file that reads must write and read back equal; then validate, flatten, and compose |
compose | flatten + compose over IFCX JSON or structured node lists with colliding paths, cycles, diamonds, and head/child edits |
validate | IfcxFile::validate with schemas that inherit each other (chains, diamonds, cycles) and nested restrictions |
pcd | PointCloud::from_pcd on arbitrary bytes (ascii, binary, binary_compressed), checked against from_pcd_base64 |
pcd_lzf | the binary_compressed LZF decoder and column-major layout behind a fixed valid header |
points_base64 | points::base64 strings and points::array JSON |
geometry | mesh, curve, transform, presentation, and point-array decoders on one JSON value |
scene_glb | RenderScene and to_glb on composed files; checks the GLB structure, alignment, index ranges, and that the JSON holds no null |
Install cargo-fuzz once (cargo install cargo-fuzz), then run a target for a bounded time:
fuzz/run.sh --list # target names
fuzz/run.sh pcd 600 # ten minutes
fuzz/run.sh compose 60 -jobs=4 # extra libFuzzer flags after the secondsfuzz/run.sh builds with the pinned nightly, debug assertions (so integer overflow panics), and ASan; reads seeds from fuzz/seeds/<target> and, for targets taking IFCX files, from the hand-written fixtures in crates/*/tests/fixtures; writes new inputs to fuzz/corpus/<target>; and limits memory to 2 GiB and each input to 10 s, so unbounded allocation and super-linear blow-ups are findings too. Plain cargo +nightly-2026-09-25 fuzz run <target> works as well.
A crash leaves its input in fuzz/artifacts/<target>/. Fix it in the library crate with a regression test in that crate's normal test suite (never only in fuzz/), and add the minimized input (cargo fuzz tmin <target> <file>) to fuzz/seeds/<target>/ when it is a useful seed. Seeds must be original work: never add buildingSMART samples.
CI runs every target in .github/workflows/fuzz.yml: one minute each on pull requests that touch the fuzzed crates or fuzz/, ten minutes each every Monday (keeping the corpus between weekly runs), and on demand with gh workflow run fuzz.yml -R openbimrs/ifcx -f seconds=1800. That workflow is not part of the required gate. When bumping the nightly date, change fuzz/rust-toolchain.toml and the workflow together.