Adding a geometric operation

SkillMonitoring & ops

This skill should be used when adding or extending a geometric shape operation in brepjs — the end-to-end recipe once the target module is chosen (which is decided by architecture-navigation) — when a task says "add a new operation", "add a fillet/draft/shell variant", "expose this Fns function in the public API", "add a method to the shape() fluent wrapper", "new function is missing from function-lookup.md", "knip flags my new export", or "where do I export this from". Covers the *Fns.ts implementation template, the six export surfaces, the function-lookup CI gate, and what to test.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Adding a geometric operation skill

What this skill tells your AI

The instructions your AI receives, as published by andymai/brepjs in .claude/skills/adding-operations/SKILL.md and read by ahel’s review.

End-to-end playbook for adding a shape operation: implement in a *Fns.ts file, surface through up to six export layers, test, and pass the docs gate. Supersedes the outline in .claude/commands/new-operation.md (whose step 7 test naming, tests/fn-<name>.test.ts, is stale — no such files exist; use tests/<moduleName>.test.ts). docs/codebase-map.md repeats the same stale fn-* names — do not copy from it.

Step 0 — Scope and home

  • Needs a new kernel capability? If OCCT must do something the KernelAdapter interface cannot express yet, add the kernel method first: follow .claude/commands/new-kernel-method.md (interface in src/kernel/types.ts / src/kernel/interfaces/*.ts, implementation in src/kernel/occt/*Ops.ts, wire src/kernel/occt/defaultAdapter.ts plus the occtWasm and brepkit adapters). See the kernel-abstraction skill. Test the new capability only through the Layer-2 API, never *Ops directly.
  • Pick the module: src/topology/ for shape-transforming ops (booleans, modifiers, healing), src/operations/ for construction ops (extrude, revolve, loft), other Layer-2 dirs per the architecture-navigation skill. Read that module's README.md first — src/topology/README.md documents the two API styles, validity brands, and six gotchas.
  • Extend before creating: prefer adding to an existing *Fns.ts (e.g. modifierFns.ts for a new modifier) over a new file.

Step 1 — Implement in *Fns.ts

New functionality goes in *Fns.ts files first (src/topology/README.md, gotcha 6). Canonical templates: src/topology/booleanFns.ts (fuse/cut with evolution tracking) and src/topology/modifierFns.ts (the draft function, with rich validation via validateDraftInputs). The five-part shape of a Fns function:

(a) Branded signature, validity-brand overloads. Take and return branded types from src/core/shapeTypes.ts. Ops that require topological validity take brands (ValidSolid, ClosedWire, ...) and offer an unsafe: true overload for trusted callers — the pattern in booleanFns.ts:

export function fuse(a: ValidSolid, b: ValidSolid, options?: BooleanOptions): Result<ValidSolid>;
export function fuse(
  a: Shape3D,
  b: Shape3D,
  options: BooleanOptions & { unsafe: true }
): Result<Shape3D>;

(b) Validate inputs → err(validationError(...)). Check null shapes, parameter bounds, empty selections. Add new codes to the BrepErrorCode as const object in src/core/errors.ts (enum is lint-banned). Constructor signature: validationError(code, message, cause?, metadata?, suggestion?) — always fill suggestion with an actionable hint. Model: validateDraftInputs in modifierFns.ts (zero-angle, out-of-range, empty face list, each with a suggestion). See the result-error-handling skill for code taxonomy.

(c) Call the kernel via getKernel(). getKernel().method(shape.wrapped, ...) — reading .wrapped as an argument is fine, but calling methods on it (or touching .oc) is ESLint-banned in Layer 2+; the architecture-navigation skill owns that rule. For shape-mutating ops, follow the evolution pattern from fuse: default trackEvolution = true, collectInputFaceHashes(inputs) + HASH_CODE_MAX, call the *WithHistory kernel variant, then propagateAllMetadata(evolution, inputs, result) on success (helpers in src/topology/metadata/metadataPropagation.ts). On history-path diagnostics errors with a null result, fuse disposes it, console.warns, and retries the plain kernel call — only console.warn/console.error pass lint.

(d) Cast and verify the result. castShape(raw) (src/core/shapeTypes.ts, defaults to 3D) then a type guard (isShape3D, isSolid, ...). On guard failure, dispose the wrapped shape before returning the error — copy castToShape3D in booleanFns.ts, which calls wrapped[Symbol.dispose]() and returns err(typeCastError(...)) naming the actual TopAbs type. Modifiers share finalizeShape3D in modifierFns.ts (cast + metadata propagation in one call).

(e) Never throw — wrap kernel calls in try/catch. Convert exceptions to err(kernelError(code, msg, cause, metadata)) with operation metadata ({ operation, faceCount, angle } in draft). One deliberate exception: abort signals rethrow (if (signal?.aborted) throw signal.reason; at the top of fuse).

Keep functions short — npm run check:patterns fails lint-staged on long functions and double-casts; extract helpers rather than adding a baseline entry (quality-gates skill).

Step 2 — Surface it (export checklist)

There are two barrels per module and they are not chained: src/topology/index.ts is a small internal Layer-2 barrel, while src/topology.ts is the published brepjs/topology sub-path entry. src/index.ts (root entry) re-exports most Fns/api symbols directly from their source files (the booleanFns and api.js blocks); a smaller set of topology helpers (cast, downcast, applyGlue, isNumber, ...) is still re-exported via the ./topology/index.js barrel. New operations belong in the direct api.js/booleanFns blocks. Work through this table top to bottom:

#SurfaceFileWhen
1Implementationsrc/<module>/<x>Fns.tsAlways
2Short-named public apisrc/topology/api.ts (or src/operations/api.ts)If it deserves a clean short name
3Root brepjs entrysrc/index.ts — add to the matching export block (the ./topology/api.js block, or the booleanFns block)Always
4Sub-path entrysrc/topology.ts / src/operations.ts / etc.If it should be importable from brepjs/topologythis is what feeds docs/function-lookup.md
5Namespace APIsrc/ns/<group>.ts (e.g. src/ns/booleans.ts re-exports from @/topology/api.js)If a matching namespace group exists
6Fluent facadesrc/topology/wrapperFns.tsIf chaining reads naturally

Notes per surface:

  • api.ts (2): accept Shapeable<T> and call resolve() (both from src/topology/apiTypes.ts); take an options object, not positional params (DraftOptions in apiTypes.ts; RotateOptions in api.ts); delegate to the Fns function passing unsafe: true when bridging a validity-brand requirement (see fuse in api.ts). Finder-style selection params type as Face[] | FinderFn<Face> | ShapeFinder<Face> resolved via resolveFaces.
  • Sub-path (4): scripts/generate-function-lookup.ts scans only the SUBPATHS map (src/core.ts, src/topology.ts, src/operations.ts, src/2d.ts, src/sketching.ts, src/query.ts, src/measurement.ts, src/io.ts, src/worker.ts, src/shapeRef.ts) — never src/index.ts. A root-only export silently never appears in docs/function-lookup.md and the gate stays green: the short api.ts names (fuse, cut, draft, fillet...) are root-only today, so none of them are in the lookup. Decide deliberately which side of that line the new op sits on.
  • brepjs/quick needs no step — src/quick.ts does export * from './index.js'.
  • wrapperFns (6): two edits — add the method to the right Wrapped/Wrapped3D interface, and to the matching factory (create3DBooleans, create3DModifiers, create3DCompoundOps, ...). Methods call the api.ts function, unwrapOrThrow (throws BrepWrapperError), and bridge validity brands via the centralized trust-casts asValidSolid / trustAsT (already carrying brepjs-patterns-disable: no-double-cast). Pattern: draft: (faces, opts) => wrap3D(trustAsT<T>(unwrapOrThrow(draftFn(asValidSolid(val), faces, opts)))). Never add a wrapper method without a Fns implementation behind it.

Step 3 — Regenerate the function lookup

If step 4 changed a sub-path entry:

npm run docs:generate-lookup

CI's build job (.github/workflows/ci.yml) regenerates, runs npx prettier --write docs/function-lookup.md, then git diff --exit-code — a stale file or a committed raw-generator (compact, un-prettified) file both fail it. lint-staged prettifies the file on commit, so regenerate-then-commit normally suffices; otherwise run prettier on it manually. Pre-commit also runs scripts/check-function-lookup.sh, a non-blocking reminder when *Fns.ts or index files are staged without the lookup.

Step 4 — Tests

Full skeleton, assertions, kernel projects, and coverage rules live in the writing-tests skill. Operation-specific minimum:

  • File: tests/<moduleName>.test.ts (e.g. draftFns.test.ts) — extend the module's existing file.
  • Happy path: isOk + unwrap, shape-kind guard, and a real measurement (unwrap(measureVolume(shape)) with toBeCloseTo).
  • Every validation branch: isErr + expect(unwrapErr(result).code).toBe('DRAFT_INVALID_ANGLE')-style code assertions.
  • Null-shape input error path (NULL_SHAPE_INPUT).
  • If behavior differs across kernels, gate via the divergence registry (skipIfDiverges / shouldSkipSuite from tests/helpers/kernelDivergences.js), never inline kernel checks.
  • Coverage functions floor is 91% — an untested exported function will fail npm run test:full.

Step 5 — Gates before commit

npm run validate   # typecheck → lint → check:boundaries → format:check → changed tests

Pre-push runs knip only. A new export nothing imports yet trips it; if the export is exercised only from tests/, tag it @testOnly in JSDoc (knip.config.ts treats the tag as used). Full gate anatomy: quality-gates skill.

Consider a playground example for user-visible ops (playground-examples skill).

Worked example — draft across every surface

SurfaceLocation
Kernel interfacesrc/kernel/interfaces/modifierOps.ts (draft), src/kernel/interfaces/evolutionOps.ts (draftWithHistory)
Fns implementationsrc/topology/modifierFns.ts, the draft function: draft(shape: ValidSolid, faces, pullDirection, neutralPlane, angle): Result<ValidSolid>; validateDraftInputs; getKernel().draftWithHistory(...); finalizeShape3D; catch → kernelError(BrepErrorCode.DRAFT_FAILED, ...)
Param typessrc/topology/apiTypes.ts (DraftOptions, DraftAngle)
Public apisrc/topology/api.ts, the draft function: draft<T extends ValidSolid>(shape: Shapeable<T>, faces, options: DraftOptions) with resolveFaces
Fluentsrc/topology/wrapperFns.ts: Wrapped3D interface entry + create3DModifiers
Root exportsrc/index.ts (inside the ./topology/api.js block)
Sub-pathabsent from src/topology.ts — hence draft does not appear in docs/function-lookup.md (the root-only-export gap, live)
Teststests/draftFns.test.ts (initKernel, error-code assertions, shouldSkipSuite)

Symptom → cause → fix

SymptomCauseFix
Direct method calls on .wrapped are banned (ESLint)Called shape.wrapped.Method() in Layer 2+Route through getKernel().method(shape.wrapped); missing kernel capability → new-kernel-method playbook
CI build job fails on git diff --exit-code docs/function-lookup.mdForgot npm run docs:generate-lookup, or committed unprettified outputRegenerate, npx prettier --write docs/function-lookup.md, commit
New function missing from function-lookup.md, gate greenExported from src/index.ts onlyExport from the sub-path entry (src/topology.ts etc.) if it belongs in the table
knip fails on pre-push for the new exportNothing outside the file imports it yetWire the remaining surfaces, or tag @testOnly if test-only
check:patterns max-function-lines on the new Fns functionValidation + kernel call + finalize inlinedExtract a validate<Op>Inputs helper and reuse finalizeShape3D/castToShape3D
Type error: Shape3D not assignable to ValidSolid at the api/wrapper layerValidity-brand overload not bridgedapi.ts passes unsafe: true; wrapperFns uses asValidSolid/trustAsT
Coverage functions threshold fails after adding the opExported function or error branch untestedCover each validation branch and the happy path (writing-tests skill)

Additional resources

  • src/topology/README.md — module map, API styles, validity brands, gotchas.
  • .claude/commands/new-kernel-method.md — kernel-layer prerequisite steps.
  • docs/which-api.md — fluent vs functional vs sketcher guidance for placement decisions.
  • Sibling skills: architecture-navigation (which layer/module), kernel-abstraction (adapters and getKernel), result-error-handling (Result/BrepError conventions), writing-tests (test skeleton and multi-kernel gating), quality-gates (validate/hooks/knip/patterns), memory-and-disposal (using/dispose-on-failure rationale), debugging-geometry (when the op returns invalid geometry), playground-examples (showcasing the op).

Signals

GitHub stars
101
Forks
9
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
adding-operations
Source
github.com/andymai/brepjs