diff options
| author | Craig Jennings <c@cjennings.net> | 2026-04-19 11:57:23 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-04-19 11:57:23 -0500 |
| commit | 18fcaf9f27d03849487078b30f667c3b574e6554 (patch) | |
| tree | 67e3ede717fbf9fbfe10c46042451e15abeeb91f /languages/elisp/claude/rules/elisp.md | |
| parent | f8c593791ae051b07dba2606c18f1deb7589825e (diff) | |
| download | rulesets-18fcaf9f27d03849487078b30f667c3b574e6554.tar.gz rulesets-18fcaf9f27d03849487078b30f667c3b574e6554.zip | |
feat: add per-project language bundles + elisp ruleset
Introduces a second install mode alongside the existing global symlinks:
per-project language bundles that copy a language-specific Claude Code
setup (rules, hooks, settings, pre-commit) into a target project.
Layout additions:
languages/elisp/ - Emacs Lisp bundle (rules, hooks, settings, CLAUDE.md)
scripts/install-lang.sh - shared install logic
Makefile additions:
make help - unified help text
make install-lang LANG=<lang> PROJECT=<path> [FORCE=1]
make install-elisp PROJECT=<path> [FORCE=1] (shortcut)
make list-languages - show available bundles
Elisp bundle contents:
- CLAUDE.md template (seed on first install, preserved on update)
- .claude/rules/elisp.md, elisp-testing.md, verification.md
- .claude/hooks/validate-el.sh (check-parens, byte-compile, run matching tests)
- .claude/settings.json (permission allowlist, hook wiring)
- githooks/pre-commit (secret scan + staged-file paren check)
- gitignore-add.txt (append .claude/settings.local.json)
Hooks use \$CLAUDE_PROJECT_DIR with a script-relative fallback, so the
same bundle works on any machine or clone path. Install activates git
hooks via core.hooksPath=githooks automatically. Re-running install is
idempotent; CLAUDE.md is never overwritten without FORCE=1.
Diffstat (limited to 'languages/elisp/claude/rules/elisp.md')
| -rw-r--r-- | languages/elisp/claude/rules/elisp.md | 75 |
1 files changed, 75 insertions, 0 deletions
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 |
