diff options
Diffstat (limited to 'languages/elisp/claude')
| -rw-r--r-- | languages/elisp/claude/hooks/validate-el.sh | 79 | ||||
| -rw-r--r-- | languages/elisp/claude/rules/elisp-testing.md | 78 | ||||
| -rw-r--r-- | languages/elisp/claude/rules/elisp.md | 75 | ||||
| -rw-r--r-- | languages/elisp/claude/rules/verification.md | 42 | ||||
| -rw-r--r-- | languages/elisp/claude/settings.json | 70 |
5 files changed, 344 insertions, 0 deletions
diff --git a/languages/elisp/claude/hooks/validate-el.sh b/languages/elisp/claude/hooks/validate-el.sh new file mode 100644 index 0000000..5fd4241 --- /dev/null +++ b/languages/elisp/claude/hooks/validate-el.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Validate and test .el files after Edit/Write/MultiEdit. +# PostToolUse hook: receives tool-call JSON on stdin. +# Silent on success; on failure, prints emacs output and exits 2 +# so Claude sees the error and can correct it. +# +# Phase 1: check-parens + byte-compile +# Phase 2: for modules/*.el, run matching tests/test-<stem>*.el + +set -u + +# 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 + printf 'PAREN CHECK FAILED: %s\n%s\n' "$f" "$output" >&2 + exit 2 + fi + ;; + *.el) + if ! output="$(emacs --batch --no-site-file --no-site-lisp \ + -L "$PROJECT_ROOT" \ + -L "$PROJECT_ROOT/modules" \ + -L "$PROJECT_ROOT/tests" \ + "$f" \ + --eval '(check-parens)' \ + --eval "(or (byte-compile-file \"$f\") (kill-emacs 1))" 2>&1)"; then + printf 'VALIDATION FAILED: %s\n%s\n' "$f" "$output" >&2 + exit 2 + fi + ;; +esac + +# --- Phase 2: test runner --- +# Determine which tests (if any) apply to this edit. +tests=() +case "$f" in + "$PROJECT_ROOT/modules/"*.el) + stem="$(basename "${f%.el}")" + mapfile -t tests < <(find "$PROJECT_ROOT/tests" -maxdepth 1 -name "test-${stem}*.el" 2>/dev/null | sort) + ;; + "$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") + ;; +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" \ + -l ert "${load_args[@]}" \ + --eval "(ert-run-tests-batch-and-exit '(not (tag :slow)))" 2>&1)"; then + printf 'TESTS FAILED for %s (%d test file(s)):\n%s\n' "$f" "$count" "$output" >&2 + exit 2 + 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..fcad9de --- /dev/null +++ b/languages/elisp/claude/rules/elisp-testing.md @@ -0,0 +1,78 @@ +# Elisp Testing Rules + +Applies to: `**/tests/*.el` + +## Framework: ERT + +Use `ert-deftest` for all tests. One test = one scenario. + +## File Layout + +- `tests/test-<module>.el` — tests for `modules/<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 for one module +- `tests/testutil-general.el`, `testutil-filesystem.el`, `testutil-org.el` — cross-module helpers + +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. + +## 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/rules/verification.md b/languages/elisp/claude/rules/verification.md new file mode 100644 index 0000000..8993736 --- /dev/null +++ b/languages/elisp/claude/rules/verification.md @@ -0,0 +1,42 @@ +# Verification Before Completion + +Applies to: `**/*` + +## The Rule + +Do not claim work is done without fresh verification evidence. Run the command, read the output, confirm it matches the claim, then — and only then — declare success. + +This applies to every completion claim: +- "Tests pass" → Run the test suite. Read the output. Confirm all green. +- "Linter is clean" → Run the linter. Read the output. Confirm no warnings. +- "Build succeeds" → Run the build. Read the output. Confirm no errors. +- "Bug is fixed" → Run the reproduction steps. Confirm the bug is gone. +- "No regressions" → Run the full test suite, not just the tests you added. + +## What Fresh Means + +- Run the verification command **now**, in the current session +- Do not rely on a previous run from before your changes +- Do not assume your changes didn't break something unrelated +- Do not extrapolate from partial output — read the whole result + +## Red Flags + +If you find yourself using these words, you haven't verified: + +- "should" ("tests should pass") +- "probably" ("this probably works") +- "I believe" ("I believe the build is clean") +- "based on the changes" ("based on the changes, nothing should break") + +Replace beliefs with evidence. Run the command. + +## Before Committing + +Before any commit: +1. Run the test suite — confirm all tests pass +2. Run the linter — confirm no new warnings +3. Run the type checker — confirm no new errors +4. Review the diff — confirm only intended changes are staged + +Do not commit based on the assumption that nothing broke. Verify. diff --git a/languages/elisp/claude/settings.json b/languages/elisp/claude/settings.json new file mode 100644 index 0000000..cca0eaa --- /dev/null +++ b/languages/elisp/claude/settings.json @@ -0,0 +1,70 @@ +{ + "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" + } + ] + } + ] + } +} |
