brepjs
CLAUDE.md140 starsChanged 5 months agoApache-2.0View on GitHub
Web CAD library with a layered architecture and pluggable kernel abstraction layer. Layered architecture with enforced boundaries (imports flow downward only): Boundaries enforced by npm run check:boundaries (runs in pre-commit and CI). Monorepo packages:
In this repository18 other files it gives its agents.
- llms-full.txtllms.txt
- llms.txtllms.txt
- adding-operationsSkill
- architecture-navigationSkill
- assembly-solverSkill
- ci-triageSkill
- companion-packagesSkill
- debugging-geometrySkill
- git-pr-workflowSkill
- kernel-abstractionSkill
- memory-and-disposalSkill
- playground-examplesSkill
- quality-gatesSkill
- release-publishingSkill
- result-error-handlingSkill
- shaperef-lineageSkill
- wasm-interopSkill
- writing-testsSkill
The file
We split this CLAUDE.md into the rules it gives an agent
8 rules. About 0 are common, 0 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 59
Layer 2+ code must never call methods on
.wrapped: always use getKernel().method(shape.wrapped). - Very specific: line 66
Never downcast() as a copy — it aliases the source id on occt-wasm.
- Very specific: line 81
No non-null assertions (!)
- Very specific: line 94
Test naming: <moduleName>.test.ts (e.g.
- Very specific: line 107
Import from @/index.js (use @/ alias for cross-directory imports, always .js extension)
- Very specific: line 108
Add beforeAll(async () => { await initKernel().
- Very specific: line 109
Use toBeCloseTo(expected, precision) for geometry.
- Very specific: line 110
Use unwrap(result) in tests.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
brepjs
Web CAD library with a layered architecture and pluggable kernel abstraction layer.
Architecture
Layered architecture with enforced boundaries (imports flow downward only):
- Layer 0 (
kernel/,utils/): Foundation, imports nothing above Layer 0 (kernel/may importutils/) - Layer 1 (
core/): Memory management, geometry, constants, imports kernel/utils only - Layer 2 (
topology/,operations/,2d/,query/,measurement/,io/,worker/): Domain, imports layers 0-1 + each other - Layer 3 (
sketching/,text/,projection/): High-level API, imports all lower layers
Boundaries enforced by npm run check:boundaries (runs in pre-commit and CI).
Packages
Monorepo packages:
brepjs(root): Core library (published)packages/brepjs-opencascade: legacy opencascade.js WASM build (deprecated; occt-wasm is the only kernel going forward, so don't publish or extend it)packages/brepjs-bim: IFC/BIM parametric building elements + IFC import/export (experimental, published)packages/brepjs-families: Declarative family layer (element trees, key paths, projection onto the CSG IR); BIM export via brepjs-bim'sfamiliesToBim(experimental, published)packages/brepjs-sheetmetal: Sheet-metal authoring + unfold/flat-pattern + DXF (experimental, published)packages/brepjs-mesh2solid: Mesh (STL/OBJ/PLY/3MF, CAD export or scan) to B-Rep solid viameshToSolid(), with its Rust→WASM engine inengine/and the committed build inwasm/(rebuild withbuild:engine; experimental, unpublished:privateand out of release-please until every tier is done)packages/brepjs-manifold: Manifold mesh/CSG preview kernel (experimental, unpublished)packages/brepjs-viewer: Shared React/R3F renderer (consumed by the playground and brepjs-cad; published, manual publish — unmanaged by release-please)packages/brepjs-cad: Agent skill + verify/preview CLI + WASM viewer (published; skill also ships via the repo Claude-plugin marketplace)packages/create-brepjs:npm create brepjsproject scaffolder (published)
Commands
npm run build: Vite library build (ES + CJS)npm run typecheck: TypeScript strict checknpm run typecheck:tests: Type-checktests/(files listed inexcludeoftsconfig.typecheck-tests.jsonstill have errors; the list only shrinks)npm run lint/npm run lint:fix: ESLintnpm run format/npm run format:check: Prettiernpm run test: Vitest (changed files only, no coverage)npm run test:full: Full test suite with coveragenpm run check:boundaries: Layer boundary enforcementnpm run knip: Unused code detectionnpm run check:patterns: AST pattern checker (double-casts, async withKernel, missingusing, long functions, deep nesting)npm run check:patterns:baseline: Regenerate pattern baseline after fixing violationsnpm run validate: typecheck + lint + boundaries + format + changed tests (in order)npx vitest run tests/booleanFns.test.ts: Run a single test filenpm run docs:generate-lookup: Regeneratedocs/function-lookup.md
Git hooks
- Pre-commit: lint-staged (ESLint + Prettier + pattern checker) + typecheck (src and tests) + boundary check (parallel), then changed-file tests (no coverage thresholds). Set
FULL_TESTS=1for full coverage run - Pre-push:
knip(unused-code detection) only. The full test suite is not run on push — CI's shardedtestjob (test:ci) is the full gate, and pre-commit already runs changed-file tests - Bypass:
--no-verify(not recommended)
Key patterns
getKernel()fromsrc/kernel/index.tsfor all kernel operations; a kernel must be registered first (init(), orregisterKernel()withOcctWasmAdapter.fromKernel(await OcctKernel.init()))registerKernel(id, adapter)/withKernel(id, fn)for custom or dual kernel usagewithKernel(id, fn)is sync-only: async callbacks silently use the wrong kernel after the firstawait. UsegetKernel(id)directly for async code.- Layer 2+ code must never call methods on
.wrapped: always usegetKernel().method(shape.wrapped). ESLint enforces this. - Branded types (
Edge,Wire,Face,Solid, etc.) insrc/core/shapeTypes.ts: lightweight handles, no class hierarchy. Types carry a phantomD extends Dimensionparameter for 2D/3D safety. - Validity brands (
ClosedWire,OrientedFace,ManifoldShell,ValidSolid) insrc/core/shapeTypes.ts: phantom types encoding topological invariants. Smart constructors (closedWire(),orientedFace()) prove validity at runtime; type guards (isClosedWire(), etc.) narrow in-place. - Default kernel: occt-wasm (
OcctKernel.init()+OcctWasmAdapter.fromKernel, externalocct-wasmnpm package).init()andbrepjs/quickresolve it first, falling back to the deprecated opencascade.js build (initFromOC,brepjs-opencascade) then brepkit WASM (BrepkitAdapter, externalbrepkit-wasm). The default test gate runs occt-wasm; run the others viaTEST_KERNEL/npm run test:occt/npm run test:brepkit. - Memory & disposal (occt-wasm is an arena kernel): shapes are arena slots; a handle's own
.delete()is a no-op, so a slot is reclaimed only viagetKernel().dispose().createHandle()(src/core/disposal.ts) routesSymbol.dispose/finalizer through the kernel, sousingfrees the slot on every kernel. Contract:using(or.delete()) every shape you own. The functional*FnsAPI is immutable and does not consume its inputs — the caller disposes them. A missedusingon an intermediate (e.g. the innerboxintranslate(box(...), ...)) leaks a slot.- Cast a fresh kernel result with
castResultShape()(notcastShape()) so its orphaned pre-downcast slot is released; usedisposeResultShape()on reject/discard branches.castResultShapeWithKnownType()for iterated sub-shapes. These are safe on in-place kernels (the guard skips identity downcasts; brepkit's dispose is a no-op). - Never
downcast()as a copy — it aliases the source id on occt-wasm. Useclone()/getKernel().copyShape()for an independently-disposable duplicate. getFaces/getEdges/getWires/getVerticesand adjacency handles are borrowed from the per-shape topology cache; it's released when the parent shape is disposed. Do not retain one past its parent's disposal —clone()/copyShapeit first.ShapeHandle.onDispose(cb)ties a dependent resource's lifetime to a shape's disposal (how the topology cache cleans up).
- Functional API in
*Fns.tsfiles: pure functions taking/returning branded types; this is the canonical surface for all shape operations. There is no class hierarchy:Vertex,Edge,Wire,Face,Shell,Solid,CompSolid,Compoundare brandedShapeHandletypes defined insrc/core/shapeTypes.ts. - New functionality goes in
*Fns.tsfiles first, then surfaces throughsrc/topology/api.ts(short-named public functions acceptingShapeable<T>) and optionally through the fluentshape()facade insrc/topology/wrapperFns.ts(chainableWrapped<T>that throws onResult.Err). Don't add operations directly towrapperFns.tswithout an*Fns.tsimplementation behind it. Result<T,E>insrc/core/result.ts: prefer over throwing in layers 2-3- All
.tsimports must use.jsextensions for ESM compatibility - Cross-directory imports use
@/alias (e.g.@/kernel/index.js); same-directory imports stay relative (./foo.js) - File naming: camelCase everywhere (no kebab-case), except a file whose main export is a Vue or React component, which is PascalCase (
ContactForm.vue,CommandPalette.tsx) - kernel/ structure: shared abstractions at root (
types.ts,interfaces/), OCCT code inocct/, brepkit code inbrepkit/ - Unused variables must be prefixed with
_
Lint rules
- No
any: use// eslint-disable-next-line @typescript-eslint/no-explicit-any -- [reason]for WASM type gaps - No non-null assertions (
!) - Consistent type imports (
import typeenforced) - No
var, strict equality,prefer-const,prefer-readonly no-unsafe-*rules disabled due to WASM type gapsno-restricted-syntaxbans.ocaccess,.wrapped.method()calls in Layer 2+ code,export leteverywhere, andenum(use anas constobject + literal union)switch-exhaustiveness-check: switches over a union/enum must handle every case (adefaultclause counts as exhaustive)ban-ts-commentrequires@ts-expect-error -- reasonformat;@ts-ignoreand@ts-nocheckare bannedno-consoleis an error (onlyconsole.error/console.warnallowed)- Pattern checker inline disable:
// brepjs-patterns-disable: <rule-id>(line above or inline)
Testing
- Tests in
/tests/, setup intests/setup.ts(WASM init) - Test naming:
<moduleName>.test.ts(e.g.shapeFns.test.ts),api*.test.tsfor public API - Vitest globals enabled, 30s timeout, forks pool,
--max-old-space-size=6144 - Coverage thresholds enforced; see
vitest.config.ts
Commits
Conventional Commits enforced: type(scope): subject
Common tasks
Writing a test
- File: extend existing
tests/<moduleName>.test.tsor create newtests/<name>.test.ts - Import from
@/index.js(use@/alias for cross-directory imports, always.jsextension) - Add
beforeAll(async () => { await initKernel(); }, 30000)and importinitKernelfrom./setup.js(it initialises the kernelTEST_KERNELselects;initOCCTpins opencascade.js and is only for tests that need that adapter) - Use
toBeCloseTo(expected, precision)for geometry; never exact equality for floating point - Use
unwrap(result)in tests; useisOk()/match()in production code - Assert shape types with
isSolid(),isFace(),isWire(), etc. - Validate geometry with
measureVolume(),measureArea()
For adding a new operation or kernel method, see .claude/commands/.
Gotchas
- OCCT Emscripten returns enum objects with
.value; extract with:typeof val === 'number' ? val : Number(val?.value ?? val) autoHealshort-circuits valid shapes:report.alreadyValid=true, no sew/heal diagnostics run- Assembly solver composes constraints down a chain: each positioning mate reads the referenced part's solved pose and resolves in topological order (a
fixednode anchors the chain). Solved mates:fixed,concentric(axis-axis),angle(plane-plane), andcoincident/distancefor plane-plane, plane-point, point-point, axis-axis, and axis-point pairs (TRANSLATIONAL_PAIRSinsolverAdapter.ts). Unsupported pairs (e.g. axis-plane) reportASSEMBLY_NOT_CONVERGEDwith the offendingtype(a-b)and DOF count. Uint32ArrayWASM interop: always convert to regularArraybefore passing to kernel methodsDisposalScopedisposes in LIFO order; register dependencies after their dependees- Arena-leak tests use occt-wasm's
getShapeCount()oracle (tests/wasmArenaDisposal.test.ts, occt-wasm-gated). It counts the whole arena, so an undisposed intermediate in the test itself reads as a false leak in the code under test —using-dispose every intermediate in a probe. JS-sidegetDisposalStats().liveHandlesis blind to orphaned pre-downcast slots; don't rely on it for arena leaks. noUncheckedIndexedAccessis enabled: array indexing returnsT | undefined, add bounds checks or use!with eslint-disableexactOptionalPropertyTypesis enabled:undefinedand missing are distinct; useprop?: T | undefinedin optional fields
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

