Quality gates

SkillDev tools

This 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.

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

GateCommandRuns in validateRuns in pre-commitElsewhere
Typechecknpm run typecheck (tsc --noEmit)1/5Tier 1 (parallel)CI typecheck
ESLintnpm run lint (eslint src/)2/5Tier 1 via lint-stagedCI lint
Boundariesnpm run check:boundaries3/5Tier 1 (:staged)CI quality
Formatnpm run format:check4/5Tier 1 via lint-stagedCI lint
Changed testsnpm run test5/5Tier 2CI test (sharded, full)
Pattern checkernpm run check:patternslint-staged (staged src only)CI quality
knipnpm run knippre-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:

SymptomRuleFix / escape
"Unexpected any"no-explicit-anyType 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-assertionNarrow with a guard; for noUncheckedIndexedAccess array reads, add a bounds check or disable-comment
"must be imported using a type-only import"consistent-type-importsUse import type { ... }
unused varno-unused-varsPrefix with _ (argsIgnorePattern/varsIgnorePattern = ^_)
"Unnecessary conditional"no-unnecessary-conditionThe condition is provably always true/false — remove it, don't disable
switch not exhaustiveswitch-exhaustiveness-checkHandle every union case; a default counts as exhaustive
"Do not use '// @ts-ignore'"ban-ts-commentUse // @ts-expect-error -- reason (the -- reason suffix is mandatory); @ts-ignore/@ts-nocheck are fully banned
console callno-consoleOnly console.error/console.warn are allowed
export let / enumno-restricted-syntaxUse 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 idFires whenThreshold / excusal
no-double-castas unknown as T, as any as T, <T><unknown>exprUse a guard, generic, or branded constructor
no-async-withkernelasync callback passed to withKernel(id, fn) (bare or member call)Use getKernel(id) for async code (kernel-abstraction skill)
require-using-for-handlescreateHandle()/createKernelHandle() not bound with usingExcused when: returned, passed directly as a call argument, or placed in an object/array literal (memory-and-disposal skill)
max-function-linesfunction body > 60 effective linesBlanks, 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/tryDepth 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 @testOnly in a JSDoc comment — tags: ['-testOnly'] treats it as used. This is the correct escape, not deleting the export.
  • ignoreExportsUsedInFile: true; duplicates and optionalPeerDependencies are off (intentional API aliases + the brepjs-opencascade optional peer).
  • Root workspace checks src/**/*.ts only. Companion workspaces (brepjs-opencascade, brepjs-voxel-wasm, brepjs-viewer, brepjs-cad, apps/playground) are fully ignored; brepjs-bim ignores examples/**. 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: test runs --changed (changed files, no coverage); test:full runs 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 test job (test:ci, no coverage) runs everything. Pre-commit runs --changed only.
  • size / benchmark: PR-only CI jobs (.size-limit.json budgets; benchmark regression vs main). Not in validate.

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