Best for
- using for a single handle
- DisposalScope — register vs track, and LIFO
- Scope helpers for Result-returning ops
andymai/brepjs/.claude/skills/memory-and-disposal/SKILL.md
This skill should be used when managing WASM handle lifetimes or hunting memory leaks in brepjs — when a task mentions "createHandle() without using keyword risks WASM memory leak", "require-using-for-handles", "Shape handle has been disposed", "kernel handle has been disposed", "memory grows / heap keeps climbing", "leaking shapes", "getDisposalStats / gcCollected", "DisposalScope register vs track", "withScopeResult", "kernelCallScoped", "registerForCleanup", "returned shape from withScope is
Decision brief
This skill should be used when managing WASM handle lifetimes or hunting memory leaks in brepjs — when a task mentions "createHandle() without using keyword risks WASM memory leak", "require-using-for-handles", "Shape handle has been disposed", "kernel handle has been disposed", "memory grows / heap keeps climbing", "leaking shapes", "getDisposalStats / gcC…
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/andymai/brepjs --skill ".claude/skills/memory-and-disposal"Inspect the Agent Skill "memory-and-disposal" from https://github.com/andymai/brepjs/blob/a307db9979ae5b70271d80b9365ff7850ea41ff2/.claude/skills/memory-and-disposal/SKILL.md at commit a307db9979ae5b70271d80b9365ff7850ea41ff2. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
WASM objects are not visible to the JS garbage collector — a leaked shape grows WASM linear memory silently. Every branded shape (Vertex, Edge, Wire, Face, Shell, Solid, CompSolid, Compound) IS a disposable handle: src/core/shapeTypes.ts wraps each via createHandle() (src/core/d…
Any branded shape works directly this way, since every shape is a ShapeHandle. Accessing .wrapped after disposal throws 'Shape handle has been disposed' (disposal.ts); the KernelHandle variant throws 'kernel handle has been disposed' (disposal.ts). Double-dispose is safe — dispo…
Any branded shape works directly this way, since every shape is a ShapeHandle. Accessing .wrapped after disposal throws 'Shape handle has been disposed' (disposal.ts); the KernelHandle variant throws 'kernel handle has been disposed' (disposal.ts). Double-dispose is safe — dispo…
Two registration methods, both return their argument for inline use:
kernelCallScoped(fn, code, message) (src/core/kernelCall.ts) creates a scope, runs fn, and disposes deterministically after return or throw — the canonical wrapper for a Fns.ts op that needs intermediate kernel allocations and returns Result:
Permission review
No configured static risk pattern was detected
This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.
Evidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 93/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 90 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
WASM objects are not visible to the JS garbage collector — a leaked shape grows WASM linear memory silently. Every branded shape (Vertex, Edge, Wire, Face, Shell, Solid, CompSolid, Compound) IS a disposable handle: src/core/shapeTypes.ts wraps each via createHandle() (src/core/disposal.ts). Cleanup has one primary rule and one safety net:
using / Symbol.dispose / DisposalScope. This is the plan.FinalizationRegistry (disposal.ts) frees handles that were never disposed — non-deterministic, GC-timed, never something to rely on. Environments lacking it get a no-op stub plus a console.warn (disposal.ts).User-facing prose lives in docs/memory-management.md (the four mechanisms, using examples, LIFO, heap-monitoring snippets, three common leak patterns) and the maintainer deep-dive in src/core/README.md. This skill adds what those lack: the tool-selection table, the pattern-checker rule, the kernel-dependent free semantics, and the leak-hunting workflow. Do not re-read those two docs for basics — point users to them.
using requires "target": "ES2022" + "lib": ["ES2022", "ESNext.Disposable"]; the repo tsconfig.json:3,6 already sets this.
| Situation | Tool | Notes |
|---|---|---|
| One temporary, freed at end of block | using x = createHandle(...) | Or using x = someFn() for any branded shape |
| Several temporaries in one function | using scope = new DisposalScope() + scope.register(...) | LIFO disposal; see ordering rule |
A Result-returning *Fns.ts op that allocates | kernelCallScoped(fn, code, msg) or withScopeResult(fn) | Scope disposed on Ok, Err, AND throw |
Async Result op | withScopeResultAsync(fn) | Note the intentional return await |
using for a single handleusing box = box(1, 1, 1);
Any branded shape works directly this way, since every shape is a ShapeHandle. Accessing .wrapped after disposal throws 'Shape handle has been disposed' (disposal.ts); the KernelHandle variant throws 'kernel handle has been disposed' (disposal.ts). Double-dispose is safe — dispose is idempotent and inner delete() failures are swallowed.
DisposalScope — register vs track, and LIFOTwo registration methods, both return their argument for inline use:
scope.register(resource) — for anything Deletable ({ delete() }); calls .delete() on dispose (disposal.ts).scope.track(disposable) — for anything with [Symbol.dispose] (branded shapes, Curve2DHandle); calls [Symbol.dispose]() (disposal.ts).DisposalScope disposes in LIFO (reverse-registration) order (disposal.ts). The ordering rule (also a CLAUDE.md gotcha): register a dependency AFTER its dependee, so the dependent is disposed first. If B is built from A, register A first and B second.
Real code overwhelmingly uses the scope directly — using scope = new DisposalScope() then scope.register(...) (e.g. src/sketching/draw3d.ts, src/operations/threadFns.ts, src/gear/gearFns.ts). Prefer this idiom.
Result-returning opskernelCallScoped(fn, code, message) (src/core/kernelCall.ts) creates a scope, runs fn, and disposes deterministically after return or throw — the canonical wrapper for a *Fns.ts op that needs intermediate kernel allocations and returns Result<AnyShape>:
return kernelCallScoped(
(scope) => {
const axis = scope.register(makeKernelAx1(origin, dir));
return getKernel().revolveVec(shape.wrapped, axis);
},
BrepErrorCode.REVOLUTION_NOT_3D,
'Revolution failed'
);
withScopeResult(fn) (disposal.ts) is the same scope discipline for any Result-returning function. withScopeResultAsync(fn) (disposal.ts) is the async variant — its body is return await fn(scope). The await is load-bearing: using disposes synchronously at end of block, so without await the scope would dispose before the promise settles. Keep the await.
These helpers are exported and documented but rarely called directly in src/; present them as available conveniences, not the dominant convention.
require-using-for-handlesThe pattern checker (scripts/check-patterns.ts, severity error) flags any createHandle() / createKernelHandle() call not bound with using. Message: "createHandle() without \using` keyword risks WASM memory leak.`"
Excused positions (check-patterns.ts):
using x = createHandle(...)return createHandle(...) — caller owns lifetimescope.register(createHandle(...)){ k: createHandle(...) } or array literal [createHandle(...)]It does not catch handles nested in ternaries/sub-expressions — those slip through. Scan scope is src/**/*.ts only (tests and scripts exempt, check-patterns.ts).
Inline disable (line above or inline):
// brepjs-patterns-disable: require-using-for-handles
Real use: createCurve2DHandle in src/core/curve2dHandle.ts, which returns the handle for the caller to own.
Where it runs: npm run check:patterns, pre-commit via lint-staged on staged src/**/*.ts, and CI's quality job. The baseline .pattern-baseline.json contains zero require-using-for-handles entries (see the quality-gates skill for the current baseline composition) — so ANY new violation of this rule fails the gate immediately; there is no baseline slack to absorb it. For baseline mechanics and the general pattern-checker workflow, see the quality-gates skill.
withScope/DisposalScope while intermediates are registered stays valid and meshable after the scope disposes. Registered intermediates are freed; the returned one is not (regression tests/withScope-disposal.test.ts, issue #723). Registering the returned value is a use-after-dispose bug.registerForCleanup(owner, deletable) / unregisterFromCleanup(deletable) (disposal.ts) — FinalizationRegistry cleanup keyed on owner. Used by Curve2D (src/2d/lib/curve2D.ts).isLive(handle) (disposal.ts) is a named !handle.disposed. Pattern: if (!isLive(h)) return err(...).createKernelHandle(ocObj) for anything Deletable (.value getter instead of .wrapped). Curve2DHandle (src/core/curve2dHandle.ts) shows the branded-wrapper pattern, including synthesizing a no-op Deletable for arena handles that lack delete().The disposal API is uniform but the WASM-side effect of .delete() depends on the active kernel. Under the default occt-wasm kernel, a handle's delete() is a no-op — the shape lives in a WASM arena and is freed only by getKernel().dispose(shape.wrapped) → k.release(id), or releaseAll() on kernel teardown. Under Embind kernels (brepjs-opencascade) .delete() genuinely frees. So using on a branded shape always gives use-after-dispose protection and stats, but only frees arena memory on Embind. This split is the single most common source of confusion when a leak reproduces on one kernel and not another.
Full file:line breakdown, the arena vs Embind table, Embind vector cleanup, and internal getKernel().dispose() call sites are in references/kernel-memory-models.md. Read it before diagnosing a kernel-specific leak.
getDisposalStats() / resetDisposalStats() (disposal.ts) return { liveHandles, peakHandles, gcCollected, scopeEnters, scopeExits }. Workflow:
resetDisposalStats() before the suspect operation.getDisposalStats() after.Symptom → meaning:
| Reading | Meaning |
|---|---|
gcCollected > 0 | Handles were reclaimed by the FinalizationRegistry — code forgot to dispose them. Each is a missed using. |
peakHandles high in a loop | Handles accumulate within an iteration; move disposal inside the loop body (a using per iteration or a per-iteration scope). |
liveHandles non-zero at rest | Suggestive, but do not assert absolute values — the FinalizationRegistry adjusts it asynchronously after a synchronous reset, so it can even go negative for later readers (tests/disposalStats.test.ts). Use it as a trend signal, not a hard number. |
For heap-monitoring snippets (performance.memory, WASM heap size), see docs/memory-management.md.
tests/disposal.test.ts is skipped on occt-wasm — describe.skipIf(shouldSkipSuite('disposal')) — because it wraps raw Embind oc objects that the default kernel does not expose. Do not add default-kernel coverage there.tests/disposalStats.test.ts (exercises stats, isLive, withScopeResult without touching oc).tests/withScope-disposal.test.ts as the exemplar for return-value lifecycle assertions.General test skeleton, geometry assertions, and kernel-skip mechanics are in the writing-tests skill.
gcWithScope, gcWithObject, and localGC were removed in commit d7e33e50 (#331). If old snippets reference them, they are gone — replace with using/DisposalScope.
.delete()), Embind vector cleanup, internal getKernel().dispose() call sites, with file:line anchors.docs/memory-management.md — user-facing cleanup guide + heap monitoring.src/core/README.md (disposal section) — maintainer deep-dive with API signatures and rationale.quality-gates (pattern-checker baseline workflow), kernel-abstraction (the .wrapped / getKernel() contract), writing-tests (test skeleton + kernel skips), result-error-handling (kernelCall / error codes), adding-operations (where cleanup fits in a new *Fns.ts).Frequently asked questions
This skill should be used when managing WASM handle lifetimes or hunting memory leaks in brepjs — when a task mentions "createHandle() without using keyword risks WASM memory leak", "require-using-for-handles", "Shape handle has been disposed", "kernel handle has been disposed", "memory grows / heap keeps climbing", "leaking shapes", "getDisposalStats / gcC…
The source record exposes this install command: npx skills add https://github.com/andymai/brepjs --skill ".claude/skills/memory-and-disposal". Inspect the command and pinned source before running it.
Alternatives
coreyhaines31/marketingskills
When the user wants to plan, design, or implement an A/B test or experiment, or build a growth experimentation program. Also use when the user mentions "A/B test," "split test," "experiment," "test this change," "variant copy," "multivariate test," "hypothesis," "should I test this," "which version is better," "test two versions," "statistical significance," "how long should I run this test," "growth experiments," "experiment velocity," "experiment backlog," "ICE score," "experimentation program
garrytan/gbrain
End-to-end discipline for turning any large data source (audio libraries, email takeouts, document corpora, chat exports, API dumps) into brain pages at scale. The lifecycle spine: SCHEMA → ACCESS → TRIAL → EVALUATE → IMPROVE → CODIFY → TEST → SKILLIFY → BULK → MONITOR. State is tracked in a durable JSON manifest (see MANIFEST-PATTERN.md) so any crash, session boundary, or subagent fan-out resumes from ground truth instead of memory.
alirezarezvani/claude-skills
App Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklist
dotnet/skills
Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", set OutputType=Exe only for test projects in Directory.Build.props, or mentions EnableMSTestRunner, EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests discovered, --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE); centralizing