@bldrs-ai/conway
v1.1598.702-gc37c5537
Published
bldrs.ai datamodel
Readme
Conway
Conway powers Bldrs Share, bringing high-quality, precision CAD to the web. This cutting-edge CAD engine, designed specifically for IFC and STEP formats, offers advanced geometric representation, enabling teams to open and visualize intricate models with exceptional accuracy and speed.
Conway includes two major subcomponents:
- IFC-gen, a mostly autogenerated TypeScript framework (~400kloc) for full API coverage parsing of the IFC 2x3, 4 compliant
*.ifcfiles, and initial support for STEP AP2xx*.stepfiles to support Automotive and 3D-printing applications. This is based on original work by hypar-io/IFC-gen, further developed by bldrs-ai for Conway. The runtime is fully open-source in this project. Please contact us for full access to our generation pipeline. - conway-geom WASM core, bldrs-ai’s rewrite of web-ifc, engineered for high-performance and to support the full suite of open CAD standards within the IFC and STEP families.
Getting Started
Codex Setup
- Ignore the below and refer to AGENTS.md.
Windows Setup
- Install MinGW-64 and add
g++.exelocation to your PATH variable.
MacOS Setup
- Install the
gmakeandnodedependencies via Homebrew (brew install gmake node).
EMSDK Setup
- Clone the EMSDK repo and add it to your path (see their instructions)
- Conway is using
6.0.2(or runscripts/setup-emsdk.sh, which installs the pinned version)
> cd $EMSDK
> ./emsdk install 6.0.2
> ./emsdk activate 6.0.2
> cd $CONWAY
conway> emcc -v
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 6.0.2
...Initial Build
Clone the Conway repository, then in the root directory of the repository:
# Make sure EMSDK environment is set up.
yarn setup
yarn build
yarn testExample Uses
You can now load your IFC files. From the Conway root:
yarn browser model.ifcyarn validator model.ifc "IFCWINDOW.OverallHeight <= 1500"
See the full example docs at Browser.md and Validator.md
Development
Update your client with changes since your last sync:
git pull
yarn setupFor the full build of both conway (TypeScript) and conway-geom (WASM subproject):
yarn buildBuild just conway, not conway-geom, e.g. for updating just the Conway API or tools:
yarn build-incrementalRunning tests without building conway-geom (no EMSDK)
The jest suite loads the conway-geom WASM binaries, which are normally produced
by the EMSDK/GENie native build. If you only touch the TypeScript, you can skip
that toolchain entirely: yarn setup builds the TypeScript (build-incremental)
and then runs yarn wasm-prebuilt, which fetches the WASM Dist/ bundle from
the last-published @bldrs-ai/conway npm package — so a fresh yarn setup && yarn
test works with no EMSDK. Pin a build with CONWAY_PREBUILT_WASM_VERSION, or
refresh with yarn wasm-prebuilt --force.
This is a convenience for TS-only work — if you change the conway-geom
submodule you must do a real yarn build to get matching binaries.
For a full, clean rebuild:
yarn build-rebuildYou can also run the tests with jest:
yarn testAnd finally, using the watch functionality, you can also have the code automatically rebuild on change and also re-run the tests using:
yarn build-test-watchIFC Parser Console Test Application
Conway has a test application for parsing IFC step files to see the performance and included entity types at src/core/ifc/ifc_command_line_main.ts.
You can set up an alias to reference the latest package or build from source and use yarn cli:
alias conway='yarn cli'
OR
alias conway='npx --package=@bldrs-ai/conway@latest exec cli'It can be run with:
conway [ifc file path]
# To output geometry
conway -g [ifc file path]
# Use -m 1024 for larger filesCLI Geometry Output Options
You can now output various geometry formats using the CLI:
conway [ifc file path] [options]Examples:
- Output all geometry (default GLTF + GLB):
conway index.ifc --geometry - Output only GLTF:
conway index.ifc --geometry --gltf - Output only GLB:
conway index.ifc --geometry --glb - Output GLTF with Draco compression:
conway index.ifc --geometry --gltf-draco - Output GLB with Draco compression:
conway index.ifc --geometry --glb-draco - Run geometry processing without saving output files:
conway index.ifc --geometry --nooutput
You can also combine flags as needed. For example:
conway index.ifc --geometry --gltf-draco --glb-dracoRun conway --help for the full list of available flags and options.
The included index.ifc in the repo is recommended for testing.
Profiling WASM Builds in Node
Profiling Conway, including building a Conway-Geom WASM binary with DWARF information and generating a flame graph with WASM symbols, is possible via the following steps:
- Run build-profile-conway_geom from Conway's package.json
- Profile your app:
node --prof --experimental-specifier-resolution=node ./compiled/src/ifc/ifc_command_line_main.js <model.ifc> -g- An isolate*.log file will be generated. Run:
node --prof-process --preprocess -j isolate*.log > v8.json # generate a V8 log- Go to https://mapbox.github.io/flamebearer/ and drop the log file to see a detailed flame graph.
Problems with renaming in GIT merges
Because of the large number of files in conway that are code changes sometimes causing large modifications in merges, especially if generation locations are changed, it's sometimes necessary to up the limit of the number of renames in the git config for merging. It can be done like so:
git config merge.renameLimit 99999You may also wish to use a low rename threshold no-commit merge strategy for some of these situations to increase likelyhood that files will be related in the merge process and to track some of the more complicated changes:
git merge -X rename-threshold=25 --no-commitCI and Testing
CI is tiered so full-corpus regression + performance cost is paid once per release candidate, not on every push. Full architecture, cost rationale, and the rc/re-bless/LFS runbook: design/new/ci-regression-cost.md.
Every PR is gated on two checks defined in .github/workflows/build.yml:
| Job | What it does |
|---|---|
| build | yarn install, WASM + TS compile (WASM cached on the conway-geom submodule SHA), yarn test, yarn lint, and a Tier-A geometry-digest check of the in-repo data/ models against committed goldens. |
| run-ifc-regression | needs: build. Reuses the same WASM cache. Runs the regression batch over the smoke subset (regression/smoke_models.txt) of the public test-models ref (TEST_MODELS_REF, default main), pinned per-run to the resolved commit SHA. Fails on any failed.csv row; digest changes are informational (reviewed via the visual-diff comment, blessed at the rc). Posts a per-PR comment with the resolved SHA + smoke-scoped failed.csv / errors.csv / perf summaries, and uploads the candidate npm tarball + perf.csv as workflow artifacts. |
A concurrency group cancels superseded PR runs (main runs are never
cancelled, so releases always complete). A merge to main re-runs those two
jobs and then chains into auto-publish (see Releases below).
The full public+private corpus runs once per release candidate — push an
rc-* tag and rc-regression.yml regenerates every baseline (opening a
reviewable baseline PR per test-models repo) while the perf-three-* jobs run
the full benchmark. See the runbook in the CI-cost doc linked above.
Regression batch
The same batch the regression CI job runs can be invoked locally — see regression/README.md for digest / verbose / batch modes and the catalog of model fixtures. CI tracks TEST_MODELS_REF near the top of build.yml (default main), resolved to a commit SHA per run and recorded in the PR comment + job summary, so each run is reproducible without relying on test-models cutting tags.
Tier A: in-repo quick-check models (data/)
The build job also runs a fast, hermetic geometry gate over every data/*.ifc model — no test-models clone and no token, so it protects every PR including forks. Each model's geometry digest (the same ifc_regression_main.js -d digest the batch produces) is diffed against its committed golden data/<name>.csv; a mismatch fails build with a re-bless hint. The goldens are checked in (generated by CI's pinned EMSDK toolchain), so the gate is live.
Re-blessing an intended change. Because the correct digest is only knowable from a CI run on the new code, the gate fails first: that same run uploads the recomputed digests as the tierA-goldens-<run_id> artifact, so you download it, commit the new data/<name>.csv, and push. One PR, but a fail-then-rebless round-trip rather than a single edit. A brand-new data/*.ifc model with no golden yet only warns (it doesn't fail) and emits its candidate digest the same way, to bootstrap.
Performance benchmarks
Tier 1 — Conway-only perf in CI (live). Every regression run emits a perf.csv of parseTimeMs / geometryTimeMs / totalTimeMs / rssMb / heapUsedMb / heapTotalMb per model. The top-10 slowest are posted in the PR comment; the full CSV is uploaded as a workflow artifact. This piggybacks on the existing regression batch so cost is ~0 extra runner minutes.
Tier 2 — full headless-three perf in CI (live). Two jobs,
perf-three-public and perf-three-private, run on rc-* tags and
workflow_dispatch (needs: run-ifc-regression) — once per release
candidate, not per merge, so per-model timings stay low-variance on the
isolated runner (see ci-regression-cost.md
for why frequency, not runner size, is the cost lever). Each downloads the candidate Conway tarball
that the regression job packed, clones headless-three
at a pinned H3_SHA, and forces the whole H3 → adapter → conway chain onto
the candidate via a yarn resolutions override (no yarn link), then runs
scripts/benchmark.cjs to time every model. The public job posts a
per-model table to the merge's PR; the private job (test-models-private,
gated on secrets.TEST_MODELS_PRIVATE_TOKEN, never on PR events so forks
can't leak the secret) posts aggregate stats only, so no private model
names appear in the public thread. Both compute a cross-version delta vs
the previous push: main run (via the .github/actions/perf-delta
composite action) and upload the detail CSV as a workflow artifact.
Releases
Releases are continuous. Every green merge to main auto-tags and
publishes to npm at the latest dist-tag. The version is
<major>.<commit>.<issue>-g<shorthash> where:
majorcomes frompackage.jsononmain(only the first segment; everything after it is recomputed on every release)commitisgit rev-list HEAD --countat the merge commit (the global commit position)issueis the first#Nin the merge commit message. Under this repo's#<issue>: description (#<pr>)title convention that is the issue number the merge closes — not the PR number, which the squash-merge appends second. A title with no issue ref yields that appended PR number instead, and a message with neither (a direct push tomain) falls back to0shorthashis the first 8 characters ofgit rev-parse HEAD, so a published version names the exact commit it was built from. It is a truncation, notgit rev-parse --short— see below
So a merge closing issue #321 at the 985th commit on main publishes
as 1.985.321-g1a2b3c4d. No PR number is encoded, and none is needed:
the hash names the commit, and the commit names its PR.
Why the commit count is the minor segment. npm refuses to publish a
version below the highest one ever published for the package unless the
dist-tag is named explicitly, and issue/PR numbers are not monotonic —
they land out of order, so the old <major>.<issue>.<commit> scheme
could compute a version below the all-time maximum and hard-fail the
release (conway#533). The commit count is strictly monotonic on main,
so with it in the minor slot every release is strictly greater than the
last. The issue number keeps its traceability value in the patch slot,
where it cannot invert the ordering.
Why the hash is truncated, not abbreviated. Every git abbreviation
— auto-abbrev and --short=N — is a minimum width that git grows
to keep the result unique against the current object database. On git
2.43, git rev-parse --short=1 HEAD returns four characters, not one;
by the same rule --short=8 starts returning nine as soon as two
objects share an eight-character prefix. Either would compute a
different version string for the same commit as the repo grows, which
is exactly the reproducibility the hash is there to provide. Truncating
the full hash depends only on the commit.
The tradeoff: a fixed truncation can collide, where a git
abbreviation cannot. That is acceptable here — the commit count in the
minor segment is already unique per commit on main, so the hash is a
convenience ref for a human, not the identifying key. A collision would
make two versions share a suffix; it would not make them equal or
misorder them.
Why -g and not a bare hash. SemVer forbids a leading zero in an
all-numeric prerelease identifier, so a short hash like 0512345 would
make the version invalid and fail the publish (~0.4% of commits). The
g prefix is the git describe convention and makes the identifier
alphanumeric. And the hash cannot be a fourth dotted segment
(1.985.321.1a2b3c4d): SemVer is exactly three numeric components and
npm rejects anything else.
Consumers must pin exactly. The -g<hash> suffix makes every
published version a SemVer prerelease, and ^/~ ranges never match
a prerelease. Share already pins conway exactly
("@bldrs-ai/conway": "1.594.1554"); any other consumer must do the
same or install via the latest dist-tag (yarn add
@bldrs-ai/conway@latest).
The auto-publish job lives in .github/workflows/build.yml and runs as
needs: [build, run-ifc-regression] on push: branches: [main]. It
parses the issue number from the merge commit message, stamps the
computed version into package.json + src/version/version.ts in
CI's working tree (not committed back), rebuilds, publishes to npm
with an explicit --tag latest, and then tags the merge commit.
The --tag latest is unconditional and has to be: npm never applies
latest implicitly to a prerelease, and every version is one now.
(The step used to probe npm view @bldrs-ai/conway@latest version and
only pass --tag latest when the new version lost the comparison. That
guard was wrong twice over — npm compares against the highest version
ever published, not against the latest dist-tag, and the probe failed
open, falling back to 0.0.0 on any error and taking the implicit
path.)
Auth: npm Trusted Publishing via GitHub OIDC. A Trusted Publisher
configured at npmjs.com on @bldrs-ai/conway is bound to this repo +
build.yml. No long-lived NPM_TOKEN secret is used; the workflow
gets a short-lived publish token per run via OIDC.
Normal flow: ship a change
- Open a PR with your change. CI runs
buildandrun-ifc-regression. - Merge once both checks are green.
- On main push, CI re-runs
build+run-ifc-regression, thenauto-publishtags and ships. Watch the workflow run in the Actions tab; the new version is on npm within ~5-10 min of the merge.
Bumping major
Edit the version field in package.json on a PR — only the first
segment matters (everything after it is recomputed). For example,
change "version": "1.0.0" to "version": "2.0.0" to start the
2.x.x line. The first auto-publish after that lands ships
2.<commit>.<issue>-g<hash>.
There is no hand-rolled minor bump — the commit count lives in that
slot — so sequential versions across the 1.x line look like
1.1556.546-g3eae7637, 1.1557.610-g9f81cd0a, etc., one per merge.
Rolling into headless-three and Share
The same flow applies to both — do H3 first, then Share:
cd $H3_DIR
git fetch upstream # or origin if not on a fork
git checkout -b conway-<VERSION> upstream/main
# Edit package.json dep for @bldrs-ai/conway to "<VERSION>"
yarn install
yarn build && yarn test
yarn serve
# Smoke test the local candidate: load all sample models, load a local model,
# exercise dialogs, etc.
git add . && git commit -m "Upgrade conway from <OLD> to <VERSION>"
git push origin HEADThen:
- Send PR for review.
- On merge, Netlify auto-builds and deploys to prod; watch the deploy logs.
- Smoke test prod (same checks as local).
- Post in
#botor#share: "New Conway in prod" with a link to the changelog.
Blessing a stable version
After Share prod is verified on a given Conway version:
# Create the GitHub release from the auto-pushed tag
gh release create <VERSION> --generate-notes
# Promote the npm package to the stable dist-tag
npm dist-tag add @bldrs-ai/conway@<VERSION> stableRoadmap
The CI / release pipeline is continuous and tiered: build (fixtures) and
run-ifc-regression (smoke subset) gate every PR; the full public+private
corpus and the headless-three perf jobs run once per rc-* release candidate;
every green merge to main auto-publishes (see Releases). The
architecture, cost rationale, and rc/re-bless/LFS runbook are in
design/new/ci-regression-cost.md. Regression
renders (the visual-diff comment) and per-model perf-in-CI shipped; the
umbrella waterfall (#316) and performance-in-CI (#314) issues are closed.
Open follow-ups
- Errors as a hard gate.
errors.csvis regenerated and reviewed (and, at the rc, blessed into the baseline), but a PR is not failed on unexpected new errors — only on parse/extract failures (failed.csv). A golden-errors gate could fail smoke-scoped error churn that isn't an intended change. - Smoke-list curation. The smoke subset (
regression/smoke_models.txt) is a hand-picked spread; as the engine's failure surface shifts, revisit which models best catch regressions cheaply. - Perf-threshold gating. The
perf-three-*jobs post deltas but don't fail an rc on a regression; a threshold could turn perf into a release gate.
