aboutsummaryrefslogtreecommitdiff
path: root/languages
diff options
context:
space:
mode:
Diffstat (limited to 'languages')
-rw-r--r--languages/elisp/CLAUDE.md68
-rwxr-xr-xlanguages/elisp/claude/hooks/validate-el.sh102
-rw-r--r--languages/elisp/claude/rules/elisp-testing.md107
-rw-r--r--languages/elisp/claude/rules/elisp.md75
-rw-r--r--languages/elisp/claude/settings.json74
-rwxr-xr-xlanguages/elisp/githooks/pre-commit50
-rw-r--r--languages/elisp/gitignore-add.txt7
-rw-r--r--languages/python/claude/rules/python-testing.md117
-rw-r--r--languages/typescript/claude/rules/typescript-testing.md214
9 files changed, 814 insertions, 0 deletions
diff --git a/languages/elisp/CLAUDE.md b/languages/elisp/CLAUDE.md
new file mode 100644
index 0000000..2562695
--- /dev/null
+++ b/languages/elisp/CLAUDE.md
@@ -0,0 +1,68 @@
+# CLAUDE.md
+
+## Project
+
+Elisp project. Customize this section with your own description, layout, and conventions.
+
+**Typical layout:**
+- `init.el`, `early-init.el` — entry points (Emacs config projects)
+- `modules/*.el` — feature modules
+- `tests/test-*.el` — ERT unit tests
+- `tests/testutil-*.el` — shared test fixtures and mocks
+
+## Build & Test Commands
+
+If the project has a Makefile, document targets here. Common pattern:
+
+```bash
+make test # All tests
+make test-file FILE=tests/test-foo.el # One file
+make test-name TEST=pattern # Match test names
+make validate-parens # Balanced parens in modules
+make validate-modules # Load all modules to verify they compile
+make compile # Byte-compile (writes .elc)
+make lint # checkdoc + package-lint + elisp-lint
+```
+
+Alternative build tools: `eldev`, `cask`, or direct `emacs --batch` invocations.
+
+## Language Rules
+
+See rule files in `.claude/rules/`:
+- `elisp.md` — code style and patterns
+- `elisp-testing.md` — ERT conventions
+- `verification.md` — verify-before-claim-done discipline
+
+## Git Workflow
+
+Commit conventions: see `.claude/rules/commits.md` (author identity,
+no AI attribution, message format).
+
+Pre-commit hook in `githooks/` scans for secrets and runs `check-parens` on
+staged `.el` files. Activate on fresh clone with `git config core.hooksPath githooks`.
+
+## Problem-Solving Approach
+
+Investigate before fixing. When diagnosing a bug:
+1. Read the relevant module and trace what actually happens
+2. Identify the root cause, not a surface symptom
+3. Write a failing test that captures the correct behavior
+4. Fix, then re-run tests
+
+## Testing Discipline
+
+TDD is the default: write a failing test before any implementation. If you can't write the test, you don't yet understand the change. Details in `.claude/rules/elisp-testing.md`.
+
+## Editing Discipline
+
+A PostToolUse hook runs `check-parens` + `byte-compile-file` on every `.el` file after Edit/Write/MultiEdit. Byte-compile warnings (free variables, wrong argument counts) are signal — read them.
+
+Prefer Write over cumulative Edits for nontrivial new code. Small functions (under 15 lines) are near-impossible to get wrong; deeply nested code is where paren errors hide.
+
+## What Not to Do
+
+- Don't add features beyond what was asked
+- Don't refactor surrounding code when fixing a bug
+- Don't add comments to code you didn't change
+- Don't create abstractions for one-time operations
+- Don't commit `.env` files, credentials, or API keys — pre-commit hook catches common patterns but isn't a substitute for care
diff --git a/languages/elisp/claude/hooks/validate-el.sh b/languages/elisp/claude/hooks/validate-el.sh
new file mode 100755
index 0000000..803badf
--- /dev/null
+++ b/languages/elisp/claude/hooks/validate-el.sh
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+# Validate and test .el files after Edit/Write/MultiEdit.
+# PostToolUse hook: receives tool-call JSON on stdin.
+#
+# On success: exit 0 silent.
+# On failure: emit JSON with hookSpecificOutput.additionalContext so Claude
+# sees a structured error in its context, THEN exit 2 to block the tool
+# pipeline. stderr still echoes the error for terminal visibility.
+#
+# Phase 1: check-parens + byte-compile
+# Phase 2: for non-test .el files, run matching tests/test-<stem>*.el
+
+set -u
+
+# Emit a JSON failure payload and exit 2. Arguments:
+# $1 — short failure type (e.g. "PAREN CHECK FAILED")
+# $2 — file path
+# $3 — emacs output (error body)
+fail_json() {
+ local ctx
+ ctx="$(printf '%s: %s\n\n%s\n\nFix before proceeding.' "$1" "$2" "$3" \
+ | jq -Rs .)"
+ cat <<EOF
+{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": $ctx}}
+EOF
+ printf '%s: %s\n%s\n' "$1" "$2" "$3" >&2
+ exit 2
+}
+
+# Portable project root: prefer Claude Code's env var, fall back to deriving
+# from this script's location ($project/.claude/hooks/validate-el.sh).
+PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}"
+
+f="$(jq -r '.tool_input.file_path // .tool_response.filePath // empty')"
+[ -z "$f" ] && exit 0
+[ "${f##*.}" = "el" ] || exit 0
+
+MAX_AUTO_TEST_FILES=20 # skip if more matches than this (large test suites)
+
+# --- Phase 1: syntax + byte-compile ---
+case "$f" in
+ */init.el|*/early-init.el)
+ # Byte-compile here would load the full package graph. Parens only.
+ if ! output="$(emacs --batch --no-site-file --no-site-lisp "$f" \
+ --eval '(check-parens)' 2>&1)"; then
+ fail_json "PAREN CHECK FAILED" "$f" "$output"
+ fi
+ ;;
+ *.el)
+ if ! output="$(emacs --batch --no-site-file --no-site-lisp \
+ -L "$PROJECT_ROOT" \
+ -L "$PROJECT_ROOT/modules" \
+ -L "$PROJECT_ROOT/tests" \
+ --eval '(package-initialize)' \
+ "$f" \
+ --eval '(check-parens)' \
+ --eval "(or (byte-compile-file \"$f\") (kill-emacs 1))" 2>&1)"; then
+ fail_json "VALIDATION FAILED" "$f" "$output"
+ fi
+ ;;
+esac
+
+# --- Phase 2: test runner ---
+# Determine which tests (if any) apply to this edit. Works for projects with
+# source at root, in modules/, or elsewhere — stem-based test lookup is the
+# common pattern.
+tests=()
+case "$f" in
+ */init.el|*/early-init.el)
+ : # Phase 1 handled it; skip test runner
+ ;;
+ "$PROJECT_ROOT/tests/testutil-"*.el)
+ stem="$(basename "${f%.el}")"
+ stem="${stem#testutil-}"
+ mapfile -t tests < <(find "$PROJECT_ROOT/tests" -maxdepth 1 -name "test-${stem}*.el" 2>/dev/null | sort)
+ ;;
+ "$PROJECT_ROOT/tests/test-"*.el)
+ tests=("$f")
+ ;;
+ *.el)
+ # Any other .el under the project — find matching tests by stem
+ stem="$(basename "${f%.el}")"
+ mapfile -t tests < <(find "$PROJECT_ROOT/tests" -maxdepth 1 -name "test-${stem}*.el" 2>/dev/null | sort)
+ ;;
+esac
+
+count="${#tests[@]}"
+if [ "$count" -ge 1 ] && [ "$count" -le "$MAX_AUTO_TEST_FILES" ]; then
+ load_args=()
+ for t in "${tests[@]}"; do load_args+=("-l" "$t"); done
+ if ! output="$(emacs --batch --no-site-file --no-site-lisp \
+ -L "$PROJECT_ROOT" \
+ -L "$PROJECT_ROOT/modules" \
+ -L "$PROJECT_ROOT/tests" \
+ --eval '(package-initialize)' \
+ -l ert "${load_args[@]}" \
+ --eval "(ert-run-tests-batch-and-exit '(not (tag :slow)))" 2>&1)"; then
+ fail_json "TESTS FAILED ($count test file(s))" "$f" "$output"
+ fi
+fi
+
+exit 0
diff --git a/languages/elisp/claude/rules/elisp-testing.md b/languages/elisp/claude/rules/elisp-testing.md
new file mode 100644
index 0000000..b5def78
--- /dev/null
+++ b/languages/elisp/claude/rules/elisp-testing.md
@@ -0,0 +1,107 @@
+# Elisp Testing Rules
+
+Applies to: `**/tests/*.el`
+
+Implements the core principles from `testing.md`. All rules there apply here —
+this file covers Elisp-specific patterns.
+
+## Framework: ERT
+
+Use `ert-deftest` for all tests. One test = one scenario.
+
+## File Layout
+
+- `tests/test-<module>.el` — tests for `<module>.el`
+- `tests/test-<module>--<helper>.el` — tests for a specific private helper (matches `<module>--<helper>` function naming)
+- `tests/testutil-<module>.el` — fixtures and mocks scoped to one module
+- `tests/testutil-*.el` — cross-module helpers (shared fixtures, generic mocks, filesystem helpers); name them for what they help with
+
+Tests must `(require 'module-name)` before the testutil file that stubs its internals, unless documented otherwise. Order matters — a testutil that defines a stub can be shadowed by a later `require` of the real module.
+
+## Test Naming
+
+```elisp
+(ert-deftest test-<module>-<function>-<scenario> ()
+ "Normal/Boundary/Error: brief description."
+ ...)
+```
+
+Put the category (Normal, Boundary, Error) in the docstring so the category is grep-able.
+
+## Required Coverage
+
+Every non-trivial function needs at least:
+- One **Normal** case (happy path)
+- One **Boundary** case (empty, nil, min, max, unicode, long string)
+- One **Error** case (invalid input, missing resource, failure mode)
+
+Missing a category is a test gap. If three cases look near-identical, parametrize with a loop or `dolist` rather than copy-pasting.
+
+## TDD Workflow
+
+Write the failing test first. A failing test proves you understand the change. Assume the bug is in production code until the test proves otherwise — never fix the test before proving the test is wrong.
+
+For untested code, write a **characterization test** that captures current behavior before you change anything. It becomes the safety net for the refactor.
+
+## Interactive vs Internal — Split for Testability
+
+When a function mixes business logic with user interaction, split it:
+
+- **Internal** (`cj/--foo`) — pure logic. All parameters explicit. No prompts,
+ no UI. Deterministic and trivially testable.
+- **Interactive wrapper** (`cj/foo`) — thin layer that reads user input and
+ delegates to the internal.
+
+```elisp
+(defun cj/--move-buffer-and-file (dir &optional ok-if-exists)
+ "Move the current buffer's file into DIR. Overwrite if OK-IF-EXISTS."
+ ...)
+
+(defun cj/move-buffer-and-file ()
+ "Interactive wrapper: prompt for DIR, delegate."
+ (interactive)
+ (let ((dir (read-directory-name "Move to: ")))
+ (cj/--move-buffer-and-file dir)))
+```
+
+Test the internal directly with parameter values — no `cl-letf` on
+`read-directory-name`, `yes-or-no-p`, etc. The wrapper gets a smoke test or
+nothing — Emacs already tests its own prompts. The internal also becomes
+reusable by other Elisp code without triggering UI.
+
+## Mocking
+
+Mock at boundaries:
+- Shell: `cl-letf` on `shell-command`, `shell-command-to-string`, `call-process`
+- File I/O when tests shouldn't touch disk
+- Network: URL retrievers, HTTP clients
+- Time: `cl-letf` on `current-time`, `format-time-string`
+
+Never mock:
+- The code under test
+- Core Emacs primitives (buffer ops, string ops, lists)
+- Your own domain logic — restructure it to be testable instead
+
+## Idioms
+
+- `cl-letf` for scoped overrides (self-cleaning)
+- `with-temp-buffer` for buffer manipulation tests
+- `make-temp-file` with `.el` suffix for on-disk fixtures
+- Tests must run in any order; no shared mutable state
+
+## Running Tests
+
+```bash
+make test # All
+make test-file FILE=tests/test-foo.el # One file
+make test-name TEST=pattern # Match by test name pattern
+```
+
+A PostToolUse hook runs matching tests automatically after edits to a module, when the match count is small enough to be fast.
+
+## Anti-Patterns
+
+- Hardcoded timestamps — generate relative to `current-time` or mock
+- Testing implementation details (private storage structure) instead of behavior
+- Mocking the thing you're testing
+- Skipping a failing test without an issue to track it
diff --git a/languages/elisp/claude/rules/elisp.md b/languages/elisp/claude/rules/elisp.md
new file mode 100644
index 0000000..e641058
--- /dev/null
+++ b/languages/elisp/claude/rules/elisp.md
@@ -0,0 +1,75 @@
+# Elisp / Emacs Rules
+
+Applies to: `**/*.el`
+
+## Style
+
+- 2-space indent, no tabs
+- Hyphen-case for identifiers: `cj/do-thing`, not `cj/doThing`
+- Naming prefixes:
+ - `cj/name` — user-facing functions and commands (bound to keys, called from init)
+ - `cj/--name` — private helpers (double-dash signals "internal")
+ - `<module>/name` — module-scoped where appropriate (e.g., `calendar-sync/parse-ics`)
+- File header: `;;; foo-config.el --- brief description -*- lexical-binding: t -*-`
+- `(provide 'foo-config)` at the bottom of every module
+- `lexical-binding: t` is mandatory — no file without it
+
+## Function Design
+
+- Keep functions under 15 lines where possible
+- One responsibility per function
+- Extract helpers instead of nesting deeply — 5+ levels of nesting is a refactor signal
+- Prefer named helpers over lambdas for anything nontrivial
+- No premature abstraction — three similar lines beats a clever macro
+
+Small functions are the single strongest defense against paren errors. Deeply nested code is where AI and humans both fail.
+
+## Requires and Loading
+
+- Every `(require 'foo)` must correspond to a loadable file on the load-path
+- Byte-compile warnings about free variables usually indicate a missing `require` or a typo in a symbol name — read them
+- Use `use-package` for external (MELPA/ELPA) packages
+- Use plain `(require 'foo-config)` for internal modules
+- For optional features, `(when (require 'foo nil t) ...)` degrades gracefully if absent
+
+## Lexical-Binding Traps
+
+- `(boundp 'x)` where `x` is a lexical variable always returns nil. Bind with `defvar` at top level if you need `boundp` to work, or use the value directly.
+- `setq` on an undeclared free variable is a warning — use `let` for locals or `defvar` for module-level state
+- Closures capture by reference. Avoid capturing mutating loop variables in nested defuns.
+
+## Regex Gotchas
+
+- `\s` is NOT whitespace in Emacs regex. Use `[ \t]` or `\\s-` (syntax class).
+- `^` in `string-match` matches after `\n` OR at position 0 — use `(= (match-beginning 0) start)` for positional checks when that matters.
+- `replace-regexp-in-string` interprets backslashes in the replacement. Pass `t t` (FIXEDCASE LITERAL) when the replacement contains literal backslashes.
+
+## Keybindings
+
+- `keymap-global-set` for global; `keymap-set KEYMAP ...` for mode-local
+- Group module-specific bindings inside the module's file
+- Autoload cookies (`;;;###autoload`) don't activate through plain `(require ...)` — use the form directly, not an autoloaded wrapper
+
+## Module Template
+
+```elisp
+;;; foo-config.el --- Foo feature configuration -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; One-line description.
+
+;;; Code:
+
+;; ... code ...
+
+(provide 'foo-config)
+;;; foo-config.el ends here
+```
+
+Then `(require 'foo-config)` in `init.el` (or a config aggregator).
+
+## Editing Workflow
+
+- A PostToolUse hook runs `check-parens` and `byte-compile-file` on every `.el` save
+- If it blocks, read the error — don't retry blindly
+- Prefer Write over repeated Edits for nontrivial new code; incremental edits accumulate subtle paren mismatches
diff --git a/languages/elisp/claude/settings.json b/languages/elisp/claude/settings.json
new file mode 100644
index 0000000..9ab9f12
--- /dev/null
+++ b/languages/elisp/claude/settings.json
@@ -0,0 +1,74 @@
+{
+ "attribution": {
+ "commit": "",
+ "pr": ""
+ },
+ "permissions": {
+ "allow": [
+ "Bash(make)",
+ "Bash(make help)",
+ "Bash(make targets)",
+ "Bash(make test)",
+ "Bash(make test *)",
+ "Bash(make test-all)",
+ "Bash(make test-unit)",
+ "Bash(make test-integration)",
+ "Bash(make test-file *)",
+ "Bash(make test-name *)",
+ "Bash(make validate-parens)",
+ "Bash(make validate-modules)",
+ "Bash(make compile)",
+ "Bash(make lint)",
+ "Bash(make profile)",
+ "Bash(emacs --batch *)",
+ "Bash(emacs -Q --batch *)",
+ "Bash(git status)",
+ "Bash(git status *)",
+ "Bash(git diff)",
+ "Bash(git diff *)",
+ "Bash(git log)",
+ "Bash(git log *)",
+ "Bash(git show)",
+ "Bash(git show *)",
+ "Bash(git blame *)",
+ "Bash(git branch)",
+ "Bash(git branch -v)",
+ "Bash(git branch -a)",
+ "Bash(git branch --list *)",
+ "Bash(git remote)",
+ "Bash(git remote -v)",
+ "Bash(git remote show *)",
+ "Bash(git ls-files *)",
+ "Bash(git rev-parse *)",
+ "Bash(git cat-file *)",
+ "Bash(git stash list)",
+ "Bash(git stash show *)",
+ "Bash(jq *)",
+ "Bash(date)",
+ "Bash(date *)",
+ "Bash(which *)",
+ "Bash(file *)",
+ "Bash(ls)",
+ "Bash(ls *)",
+ "Bash(wc *)",
+ "Bash(du *)",
+ "Bash(readlink *)",
+ "Bash(realpath *)",
+ "Bash(basename *)",
+ "Bash(dirname *)"
+ ]
+ },
+ "hooks": {
+ "PostToolUse": [
+ {
+ "matcher": "Edit|Write|MultiEdit",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-el.sh"
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/languages/elisp/githooks/pre-commit b/languages/elisp/githooks/pre-commit
new file mode 100755
index 0000000..909cde2
--- /dev/null
+++ b/languages/elisp/githooks/pre-commit
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+# Pre-commit hook: secret scan + paren validation on staged .el files.
+# Use `git commit --no-verify` to bypass for confirmed false positives.
+
+set -u
+
+REPO_ROOT="$(git rev-parse --show-toplevel)"
+cd "$REPO_ROOT"
+
+# --- 1. Secret scan ---
+# Patterns for common credentials. Scans only added lines in the staged diff.
+SECRET_PATTERNS='(AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9_-]{20,}|-----BEGIN (RSA|DSA|EC|OPENSSH|PGP)( PRIVATE)?( KEY| KEY BLOCK)?-----|(api[_-]?key|api[_-]?secret|auth[_-]?token|secret[_-]?key|bearer[_-]?token|access[_-]?token|password)[[:space:]]*[:=][[:space:]]*["'"'"'][^"'"'"']{16,}["'"'"'])'
+
+secret_hits="$(git diff --cached -U0 --diff-filter=AM \
+ | grep '^+' | grep -v '^+++' \
+ | grep -iEn "$SECRET_PATTERNS" || true)"
+
+if [ -n "$secret_hits" ]; then
+ echo "pre-commit: potential secret in staged changes:" >&2
+ echo "$secret_hits" >&2
+ echo "" >&2
+ echo "Review the lines above. If this is a false positive (test fixture, documentation)," >&2
+ echo "bypass with: git commit --no-verify" >&2
+ exit 1
+fi
+
+# --- 2. Paren check on staged .el files ---
+staged_el="$(git diff --cached --name-only --diff-filter=AM | grep '\.el$' || true)"
+
+if [ -n "$staged_el" ]; then
+ paren_fail=""
+ while IFS= read -r f; do
+ [ -z "$f" ] && continue
+ [ -f "$f" ] || continue
+ if ! out="$(emacs --batch --no-site-file --no-site-lisp "$f" \
+ --eval '(check-parens)' 2>&1)"; then
+ paren_fail="${paren_fail}${f}:
+${out}
+
+"
+ fi
+ done <<< "$staged_el"
+
+ if [ -n "$paren_fail" ]; then
+ printf 'pre-commit: paren check failed:\n\n%s' "$paren_fail" >&2
+ exit 1
+ fi
+fi
+
+exit 0
diff --git a/languages/elisp/gitignore-add.txt b/languages/elisp/gitignore-add.txt
new file mode 100644
index 0000000..72d8290
--- /dev/null
+++ b/languages/elisp/gitignore-add.txt
@@ -0,0 +1,7 @@
+# Claude Code — personal overrides (not part of the ruleset)
+/.claude/settings.local.json
+/.claude/.cache/
+
+# Elisp byte-compile artifacts (generated by the hook)
+*.elc
+*.eln
diff --git a/languages/python/claude/rules/python-testing.md b/languages/python/claude/rules/python-testing.md
new file mode 100644
index 0000000..d2342c1
--- /dev/null
+++ b/languages/python/claude/rules/python-testing.md
@@ -0,0 +1,117 @@
+# Python Testing Rules
+
+Applies to: `**/*.py`
+
+Implements the core principles from `testing.md`. All rules there apply here —
+this file covers Python-specific patterns.
+
+## Framework: pytest (NEVER unittest)
+
+Use `pytest` for all Python tests. Do not use `unittest.TestCase` unless
+integrating with legacy code that requires it.
+
+## Test Structure
+
+Group tests in classes that mirror the source module:
+
+```python
+class TestCartService:
+ """Tests for CartService."""
+
+ @pytest.fixture
+ def cart(self):
+ return Cart(user_id=42)
+
+ def test_add_item_normal(self, cart):
+ """Normal: adding an in-stock item increases quantity."""
+ cart.add("SKU-1", quantity=2)
+ assert cart.item_count("SKU-1") == 2
+
+ def test_add_item_boundary_zero_quantity(self, cart):
+ """Boundary: quantity 0 is a no-op, not an error."""
+ cart.add("SKU-1", quantity=0)
+ assert cart.item_count("SKU-1") == 0
+
+ def test_add_item_error_negative(self, cart):
+ """Error: negative quantity raises ValueError."""
+ with pytest.raises(ValueError, match="quantity must be non-negative"):
+ cart.add("SKU-1", quantity=-1)
+```
+
+## Fixtures Over Factories
+
+- Use `pytest` fixtures for test data setup
+- Use `@pytest.fixture(autouse=True)` sparingly — prefer explicit injection
+- Avoid `factory_boy` unless object graphs are genuinely complex
+- Django: prefer pytest fixtures over `setUpTestData` unless you have a
+ performance reason
+
+## Parametrize for Category Coverage
+
+Use `@pytest.mark.parametrize` to cover normal, boundary, and error cases
+concisely instead of hand-writing near-duplicate tests:
+
+```python
+@pytest.mark.parametrize("quantity,valid", [
+ (1, True), # Normal
+ (100, True), # Normal: bulk
+ (0, True), # Boundary: zero is a no-op
+ (-1, False), # Error: negative
+])
+def test_add_item_quantity_validation(cart, quantity, valid):
+ if valid:
+ cart.add("SKU-1", quantity=quantity)
+ else:
+ with pytest.raises(ValueError):
+ cart.add("SKU-1", quantity=quantity)
+```
+
+### Pairwise / Combinatorial for Parameter-Heavy Functions
+
+When `@pytest.mark.parametrize` would require listing dozens of combinations
+(feature flags × permissions × shipping × payment × etc.), switch to
+combinatorial coverage via `/pairwise-tests`. The skill generates a minimal
+matrix covering every 2-way parameter interaction — typically 80-99% fewer
+cases than exhaustive, catching most combinatorial bugs.
+
+Workflow: invoke `/pairwise-tests` → get a PICT model + generated test matrix
+→ paste the matrix into a pytest parametrize block, or use the helper to
+emit directly. The `pypict` package (`pip install pypict`) handles
+generation in-process.
+
+See `testing.md` § Combinatorial Coverage for the general rule and when
+to skip.
+
+## Mocking Guidelines
+
+### Mock these (external boundaries):
+- External APIs (`requests`, `httpx`, `boto3` clients)
+- Time (`freezegun` or `time-machine`)
+- File uploads (Django: `SimpleUploadedFile`)
+- Celery tasks (`@override_settings(CELERY_ALWAYS_EAGER=True)`)
+- Email sending (Django: `django.core.mail.outbox`)
+
+### Never mock these (internal domain):
+- ORM queries (SQLAlchemy, Django ORM)
+- Model methods and properties
+- Form and serializer validation
+- Middleware
+- Your own service functions
+
+## Async Testing
+
+Use `anyio` for async tests (not raw `asyncio`):
+
+```python
+@pytest.mark.anyio
+async def test_process_order_async():
+ result = await process_order_async(sample_order)
+ assert result.status == "processed"
+```
+
+## Database Testing (Django)
+
+- Mark database tests with `@pytest.mark.django_db`
+- Use transactions for isolation (pytest-django default)
+- Prefer in-memory SQLite for speed in unit tests
+- Use `select_related` / `prefetch_related` assertions to catch N+1 regressions
diff --git a/languages/typescript/claude/rules/typescript-testing.md b/languages/typescript/claude/rules/typescript-testing.md
new file mode 100644
index 0000000..bd6933f
--- /dev/null
+++ b/languages/typescript/claude/rules/typescript-testing.md
@@ -0,0 +1,214 @@
+# TypeScript Testing Rules
+
+Applies to: `**/*.{ts,tsx}`
+
+Implements the core principles from `testing.md`. All rules there apply here —
+this file covers TypeScript-specific patterns.
+
+## Framework: Vitest (canonical)
+
+Use Vitest for new TypeScript code. It's native ESM, native TS, fastest watch
+mode, and shares its config with Vite when the project already uses it.
+
+For legacy code, the same principles apply with different idioms:
+
+- **Jest** — same `describe`/`it`/`expect` API; substitute `jest.mock` for
+ `vi.mock`. Most patterns below port directly.
+- **Mocha + Chai** (Node backends) — `describe`/`it` are the same; assertions
+ use `expect(x).to.equal(y)` instead of `expect(x).toBe(y)`. Sinon for spies.
+- **Angular + Karma** (`ng test`) — different planet. Follow Angular's testing
+ guide; the Normal/Boundary/Error category discipline still applies.
+
+Don't mix frameworks in one package. Don't introduce a second framework to
+"try it out" — pick the one that fits and commit.
+
+## Test Structure
+
+Group tests in `describe` blocks that mirror the source module. Use
+`beforeEach` for setup that every test in the block needs:
+
+```ts
+import { beforeEach, describe, expect, it } from "vitest";
+import { Cart } from "./cart";
+
+describe("Cart", () => {
+ let cart: Cart;
+
+ beforeEach(() => {
+ cart = new Cart({ userId: 42 });
+ });
+
+ it("normal: adding an in-stock item increases quantity", () => {
+ cart.add("SKU-1", 2);
+ expect(cart.itemCount("SKU-1")).toBe(2);
+ });
+
+ it("boundary: quantity 0 is a no-op, not an error", () => {
+ cart.add("SKU-1", 0);
+ expect(cart.itemCount("SKU-1")).toBe(0);
+ });
+
+ it("error: negative quantity throws", () => {
+ expect(() => cart.add("SKU-1", -1)).toThrow(/quantity must be non-negative/);
+ });
+});
+```
+
+Co-locate test files with the source under test (`cart.ts` ↔ `cart.test.ts`)
+unless the project layout dictates a separate `tests/` tree. Match the project's
+existing convention.
+
+## Fixtures via Factory Functions
+
+TypeScript has no pytest-style fixture system. Use plain factory functions for
+test data — they're typed, refactor-safe, and explicit:
+
+```ts
+const makeUser = (overrides: Partial<User> = {}): User => ({
+ id: "u-1",
+ email: "alice@example.com",
+ role: "member",
+ ...overrides,
+});
+
+it("admin can delete posts", () => {
+ const admin = makeUser({ role: "admin" });
+ expect(canDeletePosts(admin)).toBe(true);
+});
+```
+
+Avoid `faker`/`@faker-js/faker` unless the project genuinely needs random data.
+Random fixtures hide off-by-one bugs and make test failures non-reproducible.
+Seed deterministically when faker is unavoidable.
+
+## Parametrize for Category Coverage
+
+Use `it.each` to cover normal, boundary, and error cases concisely instead of
+hand-writing near-duplicate tests:
+
+```ts
+it.each<[number, boolean]>([
+ [1, true], // Normal
+ [100, true], // Normal: bulk
+ [0, true], // Boundary: zero is a no-op
+ [-1, false], // Error: negative
+])("add(SKU-1, %i) — valid=%s", (quantity, valid) => {
+ if (valid) {
+ cart.add("SKU-1", quantity);
+ } else {
+ expect(() => cart.add("SKU-1", quantity)).toThrow();
+ }
+});
+```
+
+### Pairwise / Combinatorial for Parameter-Heavy Functions
+
+When `it.each` would require listing dozens of combinations (feature flags ×
+permissions × shipping × payment × etc.), switch to combinatorial coverage via
+`/pairwise-tests`. The skill generates a minimal matrix covering every 2-way
+parameter interaction — typically 80-99% fewer cases than exhaustive,
+catching most combinatorial bugs.
+
+Workflow: invoke `/pairwise-tests` → get a PICT model + generated test matrix
+→ paste the matrix into an `it.each` block. See `testing.md` § Combinatorial
+Coverage for the general rule and when to skip.
+
+## Mocking Guidelines
+
+### Mock these (external boundaries):
+- HTTP clients (`fetch`, `axios`, `ky`) — prefer MSW (see below) over
+ client-level mocks where possible.
+- File / filesystem APIs (`fs/promises`)
+- Time (`vi.useFakeTimers()` + `vi.setSystemTime(...)`)
+- Browser globals not covered by jsdom (`navigator.geolocation`,
+ `IntersectionObserver`, `ResizeObserver`)
+- Third-party SDKs (Stripe client, AWS SDK clients)
+
+### Never mock these (internal domain):
+- Your own service, hook, or utility functions
+- Type-narrowing helpers (`isFoo`, `assertBar`) — those are the work
+- Validation libraries (`zod`, `valibot`) — they're framework, not boundary
+- React's render lifecycle, hooks, or context — use RTL to exercise the real thing
+
+If a unit test needs heavy internal mocking, the production code needs
+restructuring (see `testing.md` § *If Tests Are Hard to Write, Refactor the
+Code*).
+
+## Async Testing
+
+Use native `async`/`await`. Don't wrap in promise chains, don't mix `done`
+callbacks:
+
+```ts
+it("processOrder resolves with status 'processed'", async () => {
+ const result = await processOrder(sampleOrder);
+ expect(result.status).toBe("processed");
+});
+```
+
+For React component tests, use RTL's `findBy*` (auto-waits) over
+`waitFor(() => getBy*)` — `findBy*` is purpose-built for the "appears
+eventually" case.
+
+## React Testing Library
+
+When testing React components:
+
+- **Query priority**: `getByRole` > `getByLabelText` > `getByPlaceholderText`
+ > `getByText`. Reach for `getByTestId` only when the others genuinely don't
+ fit. Test like the user — don't query CSS classes or DOM structure.
+- **`userEvent` over `fireEvent`**. `userEvent` simulates real interaction
+ (focus, keyboard, async). `fireEvent` is a low-level escape hatch.
+- **One assertion per behavioral concern**. A test that asserts "button
+ renders" + "button submits the form" + "form clears on submit" is three
+ tests.
+- **Don't snapshot DOM**. Snapshots rot fast and reviewers rubber-stamp them.
+ Assert specific attributes and text instead.
+
+## Network Mocking: MSW
+
+For request-level mocking in component and integration tests, use MSW (Mock
+Service Worker). It intercepts at the network layer, so the app code is
+exercised end-to-end up to the boundary:
+
+```ts
+import { http, HttpResponse } from "msw";
+import { setupServer } from "msw/node";
+
+const server = setupServer(
+ http.get("/api/orders/:id", ({ params }) =>
+ HttpResponse.json({ id: params.id, status: "shipped" })
+ )
+);
+
+beforeAll(() => server.listen());
+afterEach(() => server.resetHandlers());
+afterAll(() => server.close());
+```
+
+Prefer MSW over mocking `fetch` or `axios` directly — request mocks survive
+client-library swaps; `vi.mock("axios")` doesn't.
+
+## TypeScript-Specific Discipline
+
+- **No `any` in tests.** Tests are documentation; `any` lies about the shape.
+ Use `unknown` and narrow, or define the precise type.
+- **Prefer `satisfies` over type assertions.** `as` says "trust me"; `satisfies`
+ says "verify this conforms" without widening the inferred type.
+- **Don't disable strict checks for tests.** Same `tsconfig` as production.
+ If a test needs `// @ts-expect-error`, leave a comment explaining the
+ invariant being verified.
+- **Assertion functions for invariants.** When narrowing via runtime check is
+ expressive, write `assertIsFoo(value)` (returns `asserts value is Foo`)
+ instead of casting in every test.
+
+## Anti-Patterns (TypeScript-Specific)
+
+- Casting test data with `as Whatever` to silence a type error — fix the
+ factory or the type.
+- Using `jest.mock` patterns in a Vitest project (or vice versa) — pick one.
+- Snapshot-testing JSX trees — brittle, low-signal.
+- Testing `useState` / `useReducer` directly via `renderHook` when the same
+ behavior is reachable through the component's UI — render the component.
+- `expect(x).toBeTruthy()` when you mean `expect(x).toBe(true)` — they are
+ different invariants and the looser one masks bugs.