Quality gates
SkillDev toolsThis skill should be used when a local brepjs quality gate or `npm run validate` step fails and the specific rule's fix or escape hatch is needed — ESLint errors like "no-explicit-any" or "Direct .oc access is banned", pattern-checker "check:patterns" violations (no-double-cast, max-function-lines, require-using-for-handles), a pattern-baseline update, knip flagging an unused export, or the boundary check printing "Layer boundary violations found". Also covers the pattern-baseline trap where an unbaselined violation on main fails every open PR. Not for a red CI run in general (ci-triage) or for hook orchestration and merges (git-pr-workflow).
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 Quality gates skill
What this skill tells your AI
The instructions your AI receives, as published by andymai/brepjs in .claude/skills/quality-gates/SKILL.md and read by ahel’s review.
Pass npm run validate, understand every gate's failure output, and reach for the
right escape hatch instead of --no-verify. CLAUDE.md already lists the headline
rules and hook tiers; this skill adds the failure-output recipes, escape-hatch
mechanics, and the traps those summaries omit.
The gate map
| Gate | Command | Runs in validate | Runs in pre-commit | Elsewhere |
|---|---|---|---|---|
| Typecheck | npm run typecheck (tsc --noEmit) | 1/5 | Tier 1 (parallel) | CI typecheck |
| ESLint | npm run lint (eslint src/) | 2/5 | Tier 1 via lint-staged | CI lint |
| Boundaries | npm run check:boundaries | 3/5 | Tier 1 (:staged) | CI quality |
| Format | npm run format:check | 4/5 | Tier 1 via lint-staged | CI lint |
| Changed tests | npm run test | 5/5 | Tier 2 | CI test (sharded, full) |
| Pattern checker | npm run check:patterns | — | lint-staged (staged src only) | CI quality |
| knip | npm run knip | — | — | pre-push + CI quality |
validate is scripts/validate-change.sh: five steps, in order, fail-fast
(set -e) — it stops at the first red step and prints nothing after it. Fix
step N before step N+1 is reachable. On success it prints === All checks passed ===.
Pre-commit (.husky/pre-commit) runs Tier 1's three checks in parallel, so
their output interleaves — read carefully to attribute an error to the right gate.
For hook anatomy, FULL_TESTS=1, pre-push, commit-msg, and --no-verify, see the
git-pr-workflow skill. (Note: scripts/pre-commit-help.sh prints "coverage
thresholds enforced at push time" — that line is stale; pre-push runs only knip.)
ESLint failures
Full config: eslint.config.js (flat config, strictTypeChecked base). Rules
actually hit in practice and their sanctioned escapes:
| Symptom | Rule | Fix / escape |
|---|---|---|
| "Unexpected any" | no-explicit-any | Type it. Only for a real WASM type gap: // eslint-disable-next-line @typescript-eslint/no-explicit-any -- kernel WASM binding lacks type |
| "Forbidden non-null assertion" | no-non-null-assertion | Narrow with a guard; for noUncheckedIndexedAccess array reads, add a bounds check or disable-comment |
| "must be imported using a type-only import" | consistent-type-imports | Use import type { ... } |
| unused var | no-unused-vars | Prefix with _ (argsIgnorePattern/varsIgnorePattern = ^_) |
| "Unnecessary conditional" | no-unnecessary-condition | The condition is provably always true/false — remove it, don't disable |
| switch not exhaustive | switch-exhaustiveness-check | Handle every union case; a default counts as exhaustive |
| "Do not use '// @ts-ignore'" | ban-ts-comment | Use // @ts-expect-error -- reason (the -- reason suffix is mandatory); @ts-ignore/@ts-nocheck are fully banned |
| console call | no-console | Only console.error/console.warn are allowed |
export let / enum | no-restricted-syntax | Use a getter fn or const; use as const object + literal union |
Two Layer-2+ bans — "Direct .oc access is banned" and "Direct method calls on
.wrapped are banned" — fire in the domain/high-level dirs. The fix for both is
getKernel().method(shape.wrapped), never a disable-comment. The rule mechanics
and the exact directory list live in the architecture-navigation skill; for the
correct kernel-method call pattern see kernel-abstraction.
npm run lint only covers src/. Tests are linted at commit time via lint-staged
(.lintstagedrc.json), not by npm run lint. Prettier config (.prettierrc.json):
semicolons, single quotes, trailingComma: es5, width 100, LF.
Pattern checker deep-dive
scripts/check-patterns.ts — AST checks ESLint can't express. Five rules, all
severity error:
| Rule id | Fires when | Threshold / excusal |
|---|---|---|
no-double-cast | as unknown as T, as any as T, <T><unknown>expr | Use a guard, generic, or branded constructor |
no-async-withkernel | async callback passed to withKernel(id, fn) (bare or member call) | Use getKernel(id) for async code (kernel-abstraction skill) |
require-using-for-handles | createHandle()/createKernelHandle() not bound with using | Excused when: returned, passed directly as a call argument, or placed in an object/array literal (memory-and-disposal skill) |
max-function-lines | function body > 60 effective lines | Blanks, comments, and lone-brace lines don't count; extract helpers |
max-nesting-depth | > 4 levels of if/for/for-in/for-of/while/do/switch/try | Depth resets inside nested function bodies; use early returns |
The rule id is require-using-for-handles (not "missing using") — that exact string
goes in baselines and disable-comments.
Inline disable: // brepjs-patterns-disable: <rule-id> on the line above the
violation or inline on the same line. <rule-id> may be a specific rule or * for
all rules on that line.
Baseline (.pattern-baseline.json, version-2): only new (non-baselined)
violations fail; exit 1 if any. Currently 30 entries (15 max-function-lines,
14 no-double-cast, 1 max-nesting-depth). Fingerprints are content-based
(file|rule|normalized-80char-snippet|#occurrenceIdx), so:
- Editing code above a violation does NOT reshuffle fingerprints.
- Editing the violating line itself (rename, signature change, cast text) changes the fingerprint → it reports as "new". This is intended: touch it, own it.
Regenerate after intentionally adding/removing violations:
npm run check:patterns:baseline (= --update-baseline). Other flags:
--no-baseline (report everything, ignore baseline), --json, --sarif.
Only src/**/*.ts is ever scanned (no .d.ts, no src/kernel/wasm/). Console
failure output ends with the two escape hatches printed verbatim: the disable-comment
format and the --update-baseline command.
TRAP: an unbaselined violation on main fails every open PR
CI's quality job runs check:patterns over all of src/ and checks out the
PR-merged-with-main commit. So a max-function-lines/no-double-cast violation that
lands on main without a matching baseline entry fails the quality job — and thus
the required ci-pass aggregate — on every open PR at once, even PRs that never
touched that file.
Recovery: land a tiny baseline-bump PR first (npm run check:patterns:baseline,
commit .pattern-baseline.json). That PR passes because its own merge commit contains
the new baseline; once merged, the other PRs go green. This happens because
lint-staged only runs the pattern checker on staged files locally, so a long
function can slip in via a large refactor and only bite in full-src CI. See the
ci-triage skill for other quality-job failure modes.
TRAP: function-lookup CI diff after adding a *Fns.ts export
Adding a *Fns.ts export makes CI's build job flag a docs/function-lookup.md diff unless the file is regenerated and prettier-normalized before commit — full recipe in the adding-operations skill.
knip (unused exports)
Runs on pre-push and in CI's quality job. Config: knip.config.ts.
- Export used only by tests? knip can't trace
tests/(separate tsconfig,@/alias). Tag it@testOnlyin a JSDoc comment —tags: ['-testOnly']treats it as used. This is the correct escape, not deleting the export. ignoreExportsUsedInFile: true;duplicatesandoptionalPeerDependenciesare off (intentional API aliases + thebrepjs-opencascadeoptional peer).- Root workspace checks
src/**/*.tsonly. Companion workspaces (brepjs-opencascade,brepjs-voxel-wasm,brepjs-viewer,brepjs-cad,apps/playground) are fully ignored;brepjs-bimignoresexamples/**. See companion-packages.
Boundary check
scripts/check-layer-boundaries.sh enforces downward-only imports (target layer ≤
source layer), handling both @/ alias and relative imports. Violation output:
Layer boundary violations found:
VIOLATION: src/topology/foo.ts (layer 2: topology) imports from '@/sketching/bar.js' (layer 3: sketching)
The script's layer map is a superset of the CLAUDE.md table — it also places
csg/voxel/implicit in Layer 2 and gear/ns/lattice in Layer 3. Unrecognized
top-level dirs and root files (src/index.ts) are skipped. --staged mode
(check:boundaries:staged, used by pre-commit) checks only staged files; env
BOUNDARY_SRC_DIR overrides the scan root for fixture testing. For where new code
belongs and how to restructure to fix a violation, see architecture-navigation.
Not gated locally (surfaces only in CI)
- Coverage:
testruns--changed(changed files, no coverage);test:fullruns the whole suite with--coverage. Thresholds run on main pushes only,continue-on-error— informational, never a PR gate. See writing-tests. - Full test suite: only the sharded CI
testjob (test:ci, no coverage) runs everything. Pre-commit runs--changedonly. - size / benchmark: PR-only CI jobs (
.size-limit.jsonbudgets; benchmark regression vs main). Not invalidate.
Additional resources
Adjacent skills own the neighboring facts: git-pr-workflow (hook tiers,
conventional commits, --no-verify), architecture-navigation (layer map, the
.oc/.wrapped rule + directory list), kernel-abstraction (withKernel vs
getKernel), memory-and-disposal (using handles), ci-triage (CI job failure
modes), writing-tests (coverage and the vitest runner config), adding-operations
(function-lookup gate), companion-packages (workspace layout).
Signals
- GitHub stars
- 101
- Forks
- 9
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
quality-gates- Source
- github.com/andymai/brepjs