aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x.claude/hooks/validate-el.sh114
l---------.claude/rules/commits.md1
-rw-r--r--.claude/rules/elisp-testing.md157
-rw-r--r--.claude/rules/elisp.md75
l---------.claude/rules/testing.md1
l---------.claude/rules/verification.md1
-rw-r--r--.claude/settings.json74
-rw-r--r--.gitignore3
-rwxr-xr-xgithooks/pre-commit50
9 files changed, 3 insertions, 473 deletions
diff --git a/.claude/hooks/validate-el.sh b/.claude/hooks/validate-el.sh
deleted file mode 100755
index d6999ac..0000000
--- a/.claude/hooks/validate-el.sh
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/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), always sent to Claude in additionalContext
-# $4 — optional compact terminal echo; when set, the terminal shows this
-# instead of the full $3 (Claude still gets the full $3). Used by the
-# test runner so a failing suite prints a short summary to the pane
-# rather than dumping every ERT backtrace.
-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" "${4:-$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" \
- -L "$PROJECT_ROOT/themes" \
- --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" \
- -L "$PROJECT_ROOT/themes" \
- --eval '(package-initialize)' \
- -l ert "${load_args[@]}" \
- --eval "(ert-run-tests-batch-and-exit '(not (tag :slow)))" 2>&1)"; then
- # Terminal gets a compact summary (the run tally + the failing test names);
- # Claude still gets the full backtrace via additionalContext. Keeps the
- # pane from drowning in ERT stack frames on every red test.
- summary="$(printf '%s\n' "$output" \
- | grep -E '^Ran [0-9]+ tests|unexpected results:|^[[:space:]]+FAILED' || true)"
- [ -n "$summary" ] && summary="${summary}"$'\n'"(full backtrace in Claude's context)"
- fail_json "TESTS FAILED ($count test file(s))" "$f" "$output" "$summary"
- fi
-fi
-
-exit 0
diff --git a/.claude/rules/commits.md b/.claude/rules/commits.md
deleted file mode 120000
index 3e746ed..0000000
--- a/.claude/rules/commits.md
+++ /dev/null
@@ -1 +0,0 @@
-/home/cjennings/code/rulesets/claude-rules/commits.md \ No newline at end of file
diff --git a/.claude/rules/elisp-testing.md b/.claude/rules/elisp-testing.md
deleted file mode 100644
index 7c3a9ef..0000000
--- a/.claude/rules/elisp-testing.md
+++ /dev/null
@@ -1,157 +0,0 @@
-# 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.
-
-### Measuring it — `make coverage-summary`
-
-The bundle ships a coverage summary at `.claude/scripts/coverage-summary.el` and a Makefile fragment (`coverage-makefile.txt`) with `coverage` and `coverage-summary` targets. After `make coverage` writes an undercover SimpleCov report, `make coverage-summary` prints a per-file table and a unit-weighted project number.
-
-The number to watch is the missing-file count. A module no test loads never appears in the SimpleCov report, so a line-weighted total skips it silently — the suite looks healthier than it is. The summary counts every `modules/*.el` on disk that's absent from the report as 0%, so an untested module drags the project number down where you can see it. Copy the fragment's targets into your own Makefile to adopt it; the bundle never edits your Makefile.
-
-## 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.
-
-## Batch-Mode Reproducibility
-
-Tests must pass under `emacs --batch` — the headless, scriptable path that CI and the `make` targets use. `--batch` is the source of truth, not an interactive session.
-
-- Don't depend on interactive-session state: window configuration, frame parameters, `this-command`, minibuffer activity, or anything a running editor accumulates. A test that passes in a live Emacs but fails (or hangs) under `--batch` is broken.
-- Don't block on a prompt. `--batch` has no one to answer `y-or-n-p` or `read-string`, so an unmocked prompt either errors or stalls the run. Test the internal directly (see *Interactive vs Internal* above) or `cl-letf` the prompt.
-- Keep tests deterministic: no reliance on test execution order, wall-clock time (mock `current-time`), or environment that differs between the developer's machine and CI.
-
-## Isolating Emacs State
-
-A test must not read or mutate the developer's real Emacs config. Bind a throwaway environment so the run is hermetic regardless of who runs it.
-
-- Bind `user-emacs-directory` (and, when relevant, `user-init-file`) to a temp directory so package state, `custom-file` writes, caches, and auto-save files land in the sandbox rather than the developer's `~/.emacs.d`.
-- Control `load-path` explicitly. Add only the project's own directories; don't lean on whatever happens to be installed in the developer's session.
-- Depend only on the project's declared dependencies. A test that passes because some unrelated package is installed on this machine will fail on a clean checkout or in CI.
-
-```elisp
-(ert-deftest test-foo-writes-to-sandbox ()
- "Normal: writes under an isolated user-emacs-directory."
- (let* ((sandbox (make-temp-file "elisp-test-" t))
- (user-emacs-directory (file-name-as-directory sandbox)))
- (unwind-protect
- (progn
- (cj/--foo)
- (should (file-exists-p (expand-file-name "foo.cache" user-emacs-directory))))
- (delete-directory sandbox t))))
-```
-
-## Byte-Compile and Native-Comp Warnings
-
-A clean compile is part of green. Byte-compile warnings (free variables, wrong argument counts, unused lexical bindings, obsolete-function calls) flag real defects, so treat them as failures rather than noise.
-
-This can be enforced in the test run by binding `byte-compile-error-on-warn` to `t` and compiling the modules under test, optionally extending to native compilation where `native-comp-async-report-warnings-errors` is available.
-
-Keep the native-comp half conditional. Native compilation exists only on builds with the `native-compile` feature (Emacs 28+ compiled with it); older or non-native builds lack `native-comp-*` variables and `native-compile` entirely. Gate on the feature so the suite still runs everywhere:
-
-```elisp
-(when (and (fboundp 'native-comp-available-p) (native-comp-available-p))
- ;; native-comp-specific checks here
- )
-```
-
-Make the warnings-as-errors gate opt-in or version-aware rather than absolute — a warning that's clean on the project's pinned Emacs may differ across versions, and a hard failure on every build penalizes contributors on a different Emacs than the maintainer's.
-
-## 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/.claude/rules/elisp.md b/.claude/rules/elisp.md
deleted file mode 100644
index ea9bdc2..0000000
--- a/.claude/rules/elisp.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# 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
-- Edit cohesively, then verify parens/byte-compile right away. For nontrivial Elisp, land a function as one complete, coherent change rather than dribbling it in over many tiny partial edits — incremental fragments accumulate subtle paren mismatches. Run the paren-balance and byte-compile checks immediately after editing, whatever editing mechanism the environment uses.
diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md
deleted file mode 120000
index 23c3a14..0000000
--- a/.claude/rules/testing.md
+++ /dev/null
@@ -1 +0,0 @@
-/home/cjennings/code/rulesets/claude-rules/testing.md \ No newline at end of file
diff --git a/.claude/rules/verification.md b/.claude/rules/verification.md
deleted file mode 120000
index ac32768..0000000
--- a/.claude/rules/verification.md
+++ /dev/null
@@ -1 +0,0 @@
-/home/cjennings/code/rulesets/claude-rules/verification.md \ No newline at end of file
diff --git a/.claude/settings.json b/.claude/settings.json
deleted file mode 100644
index 9ab9f12..0000000
--- a/.claude/settings.json
+++ /dev/null
@@ -1,74 +0,0 @@
-{
- "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/.gitignore b/.gitignore
index a869e43..d08a1bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,6 @@
.claude/
/chime-autoloads.el
/tests/tests-autoloads.el
+CLAUDE.md
+githooks/
+/coverage-makefile.txt
diff --git a/githooks/pre-commit b/githooks/pre-commit
deleted file mode 100755
index 909cde2..0000000
--- a/githooks/pre-commit
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/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