Navigating the layered architecture
SkillFiles & storageThis skill should be used when deciding which layer or module a new file, function, or directory belongs in, or when a layer-boundary check fails. It owns the placement decision (which layer/module) and the import-direction rules — not the end-to-end recipe for wiring an operation through the API ladder (that is adding-operations). Trigger phrases include "where should this file/function go", "which layer is X in", "can topology import from sketching", "layer boundary violation", "check:boundaries failed", "VIOLATION: src/... imports from", "Direct .oc access is banned", "method calls on .wrapped are banned", or adding a new src/ directory or module.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Navigating the layered architecture skill
What this skill tells your AI
The instructions your AI receives, as published by andymai/brepjs in .claude/skills/architecture-navigation/SKILL.md and read by ahel’s review.
brepjs enforces a four-layer architecture where imports flow downward or sideways, never upward. The enforcement script is the source of truth for layer membership — CLAUDE.md and docs/architecture.md summaries lag behind it.
Layer table (source of truth: scripts/check-layer-boundaries.sh)
| Layer | Directories | May import from |
|---|---|---|
| 0 | kernel/, utils/ | nothing internal (same-layer only) |
| 1 | core/ | layer 0 |
| 2 | topology/, 2d/, operations/, query/, measurement/, io/, worker/, csg/, voxel/, implicit/ | layers 0–1 + each other |
| 3 | sketching/, text/, projection/, gear/, ns/, lattice/ | layers 0–2 + each other |
Rules and caveats:
- The rule is
target_layer <= source_layer(theif (( target_layer > src_layer ))check inscripts/check-layer-boundaries.sh). Same-layer imports are always legal — e.g.src/topology/wrapperFns.tsimports@/operations/api.jsand@/measurement/measureFns.js(both Layer 2). - Files at the
src/root (index.ts,quick.ts, sub-path entries liketopology.ts,core.ts,result.ts) are exempt — the script assigns them layer-1and skips them. They exist to re-export everything. - The script resolves both
@/alias and relative imports, but only grepsfrom '...'statements — dynamicimport()expressions withoutfromare not checked. Do not rely on that gap. - When adding a new top-level
src/directory, add it to thecasestatement in theget_layer()function inscripts/check-layer-boundaries.shand to the error-message layer listing at the bottom of the same script, or its imports go entirely unchecked.
Decision procedure: where does new code belong?
Work through this list in order; stop at the first match.
- A raw kernel/OCCT API call (anything touching
oc.*or WASM objects) → a*Ops.tsfile undersrc/kernel/occt/(orsrc/kernel/brepkit/), exposed as a method onKernelAdapterinsrc/kernel/types.ts. Follow the/new-kernel-methodcommand in.claude/commands/. See the kernel-abstraction skill. - A pure helper with no geometry dependency (string/array/math utilities) →
src/utils/. - Shared types,
Result, vectors, memory/disposal, branded shape types →src/core/(e.g.src/core/result.ts,src/core/shapeTypes.ts,src/core/disposal.ts). - A shape operation (transform, boolean, modifier, query, measurement, I/O) → the matching Layer 2 module's
*Fns.tsfile, then climb the API ladder below. See the adding-operations skill for the full recipe. - High-level sugar composing Layer 2 operations (sketch DSL, text, projection, gear generators, namespace re-exports) → the matching Layer 3 module.
- In doubt between two layers → put it in the lower layer that has everything it needs. Code can always be re-exported upward; it can never import upward.
The API ladder (Layer 2 shape operations)
New functionality goes in *Fns.ts first, then surfaces upward. The full chain (mermaid diagram in src/topology/README.md):
shape() fluent facade (wrapperFns.ts)
→ api.ts (functional public API)
→ *Fns.ts implementations
→ cast.ts / getKernel()
Checklist when adding an operation:
-
Implement in the module's
*Fns.ts(e.g.src/topology/booleanFns.ts), taking/returning branded types fromsrc/core/shapeTypes.ts, returningResult<T, E>for fallible operations (see the result-error-handling skill). -
Add a short-named wrapper in
src/topology/api.tsthat acceptsShapeable<T>and delegates viaresolve()(both fromsrc/topology/apiTypes.ts), using an options object for optional parameters. Pattern:export function translate<T extends AnyShape<Dimension>>(shape: Shapeable<T>, v: Vec3): T { return transforms.translate(resolve(shape), v); } -
Optionally add a chainable method on
Wrapped<T>insrc/topology/wrapperFns.ts— it must delegate toapi.ts, never contain its own implementation. -
Export from
src/index.ts(organized by layer with// ── Layer N ──banners) and the matching sub-path entry atsrc/root (topology.ts,operations.ts, etc. — table indocs/codebase-map.md). -
Update the module's
README.mdif one exists (pre-commit prints a non-blocking reminder viascripts/check-readme-reminders.sh).
Import and naming rules
| Rule | Enforced by |
|---|---|
| Imports flow downward/same-layer only | check:boundaries (pre-commit, CI, npm run validate) |
Cross-directory imports use @/ alias (@/kernel/index.js); same-directory imports stay relative (./foo.js) | convention only |
All .ts imports use .js extensions (ESM; Vite transforms at build time) | convention only — typecheck passes without it, so review for it explicitly |
| camelCase filenames (no PascalCase, no kebab-case) | convention only |
import type for type-only imports | ESLint consistent-type-imports |
The @/ alias maps to ./src/* via tsconfig.json paths and vite.config.ts resolve.alias.
The kernel-abstraction rule
Layer 2+ code treats shapes as opaque handles. Read shape.wrapped only to pass it into a kernel method — never call methods on it, and never touch .oc:
// Correct: .wrapped passed as an argument
const volume = getKernel().volume(shape.wrapped);
// Banned: method called ON .wrapped
const hash = shape.wrapped.HashCode(1000);
ESLint enforces both bans via the no-restricted-syntax rule in the "Kernel abstraction boundary" config block in eslint.config.js, but only for ten enumerated directories: topology/, operations/, measurement/, query/, io/, 2d/, sketching/, projection/, text/, worker/. The rule applies just as much in csg/, voxel/, implicit/, gear/, ns/, and lattice/ — lint simply does not cover them yet, so apply it by discipline there (and add new directories to the ESLint file list when creating them).
If an operation needs a kernel capability that KernelAdapter lacks, the fix is never to reach through .wrapped — add the method to the adapter first (kernel-abstraction skill, /new-kernel-method command).
Fixing violations
| Symptom | Cause | Fix |
|---|---|---|
VIOLATION: src/operations/foo.ts (layer 2: operations) imports from '@/sketching/draw.js' (layer 3: sketching) from check:boundaries | Lower layer imports a higher one | One of: (a) move the importing code up to the higher layer, (b) extract the shared logic down into a layer both can import (usually core/ or a Layer 2 sibling), (c) invert the dependency — have the higher layer pass data/callbacks down |
Direct .oc access is banned in Layer 2+ code (ESLint) | Raw OCCT call outside kernel/ | Move the call into a kernel *Ops.ts file behind a KernelAdapter method, then call getKernel().method(...) |
Direct method calls on .wrapped are banned in Layer 2+ code (ESLint) | Treating a shape handle as an object with behavior | Find (or add) the equivalent KernelAdapter method in src/kernel/types.ts and call it via getKernel() |
| Boundary check passes locally but fails in CI | Pre-commit runs only the staged variant (check:boundaries:staged, .husky/pre-commit Tier 1); CI's quality job runs the full tree | Run npm run check:boundaries (full) before pushing |
| New directory's imports never flagged | Directory missing from the script's case statement (layer -1 is skipped) | Register it in scripts/check-layer-boundaries.sh |
Reproduce locally:
npm run check:boundaries # full tree
npm run check:boundaries:staged # staged files only (what pre-commit runs)
npx eslint src/ --quiet # .oc / .wrapped bans
npm run validate # typecheck + lint + boundaries + format + changed tests
Where each gate runs: pre-commit Tier 1 (staged boundaries, parallel with lint-staged and typecheck), CI quality job in .github/workflows/ci.yml (full boundaries + check:patterns + knip), and scripts/validate-change.sh via npm run validate. See the quality-gates skill for the full gate matrix.
Additional resources
docs/architecture.md— layer diagrams, data flow, key patterns with correct/banned.wrappedexamples (layer lists predatecsg/voxel/implicit/gear/ns/lattice; trust the script).docs/codebase-map.md— sub-path entry-point table and module→key-file maps.docs/which-api.md— choosing between the fluent wrapper, Sketcher, functional API, and Drawing as a consumer.CONTRIBUTING.md— "Layer Boundaries" and "ESM Imports" sections, including the boundary-error walkthrough.- Module READMEs exist for
2d,core,io,kernel,kernel/manifold,measurement,operations,projection,query,sketching,text,topology,utils— read the target module's README before adding code there. (worker,csg,voxel,implicit,gear,ns,latticehave none yet.) - ADRs for the "why":
docs/decisions/0001-layered-architecture.md,0002-kernel-abstraction.md,0006-domain-boundaries.md,0007-kernel-interface-segregation.md. - Sibling skills:
adding-operations(end-to-end operation recipe),kernel-abstraction(adapter methods,getKernel/withKernel),result-error-handling(Result<T,E>conventions),quality-gates(all local/CI checks).
Signals
- GitHub stars
- 101
- Forks
- 9
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
architecture-navigation- Source
- github.com/andymai/brepjs