关于 clj-debug
clj-debug 是面向 Clojure 开发的 REPL 调试技能,指导开发者在 nrepl 中检查失败点的值并即时验证修复,而不是先修改源码添加日志。它还说明使用 Integrant 组件时,何时需要重置运行中的组件。
主要功能
- 01在 nrepl 中捕获并检查变量
- 02不改源码测试修复方案
- 03依据错误与堆栈定位问题
- 04处理 Integrant 组件重置
- 05提交前移除临时 def
适用场景
- 01定位函数参数或调用约定错误
- 02在写入源码前验证修复
- 03排查组件仍呈现旧行为的情况
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ humorless/ clj-native-agent --skill clj-debug安装后在 Claude Code 中输入/clj-debug确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
CHANGELOG.mdIMPROVEMENT_SUMMARY.mdevals/evals.jsoniteration-1-grading.mdSIDE_BY_SIDE_COMPARISON.md详细介绍
为什么使用基于 REPL 的 Clojure 调试
添加日志并重新运行测试会造成缓慢的反馈循环。Clojure 的 REPL 让你可以在出错的位置直接检查值、即时测试修复,并在修改源文件前验证更正。这比 println 或正式调试器更快、更安全。
核心模式:先暂停,别记录日志
**危险信号:**你正准备添加 println、tap>、(log ...),或修改源代码来调试。
**改为:**在 nrepl 中使用内联 def。直接求值修改后的代码,无需更改文件;捕获并检查值,测试修复,然后放心提交。
内联 Def 模式:捕获并检查
函数出错时,在函数内部临时添加 (def variable-name variable-name),并在 nrepl 中求值修改后的函数。这样无需更改源文件即可捕获该值。
示例:发现调用约定错误
你的函数运行失败:
(defn foo [& [{:keys [a b c] :as m}]]
(+ a b c))
(foo :a 1 :b 2 :c 3)
;; => java.lang.NullPointerException
在 nrepl 中,给函数添加内联 def,以捕获参数:
;; Evaluate this in nrepl (don't modify the source file):
(defn foo [& [{:keys [a b c] :as m}]]
(def m m) ;; TODO: remove before commit
(+ a b c))
;; Now call it:
(foo :a 1 :b 2 :c 3)
;; => java.lang.NullPointerException
;; Inspect what was captured:
m
;; => :a
;; Aha! m is :a, not a map. The caller is passing wrong format.
现在你明白错误所在了:该函数需要的是映射 {:a 1 :b 2 :c 3},而不是关键字参数。
示例:在 nrepl 中测试修复后再提交
了解问题后,在 nrepl 中测试修正后的版本:
;; Test 1: Fix the calling convention
(foo {:a 1 :b 2 :c 3})
;; => 6 ✓ Works!
;; Test 2: Or fix the function to accept varargs correctly
(defn foo [& [{:keys [a b c] :as m}]]
;; No def here — we understand the bug now
(+ a b c))
(foo {:a 1 :b 2 :c 3})
;; => 6 ✓ Verified!
;; Or change the function signature:
(defn foo [& args]
(def args args) ;; Capture to verify varargs format
(let [{:keys [a b c]} (first args)]
(+ a b c)))
(foo {:a 1 :b 2 :c 3})
;; args => ({:a 1, :b 2, :c 3}) ✓ Correct format now
修复在 nrepl 中生效后,就可以放心更新源文件。提交前移除所有 (def ...) 行。
发现问题与修复工作流
以下是完整流程——使用 /brepl 逐步求值:
digraph debugging_workflow {
rankdir=TB;
node [shape=box, style=rounded];
edge [fontsize=10];
"Test Fails or\nError Occurs" -> "Read Error\nMessage &\nStack Trace";
"Read Error\nMessage &\nStack Trace" -> "Identify Where\nto Inspect\n(Which function/\nwhich args?)";
"Identify Where\nto Inspect\n(Which function/\nwhich args?)" -> "Write Function\nwith inline def\nin nrepl\n(def var var)";
"Write Function\nwith inline def\nin nrepl\n(def var var)" -> "Evaluate Modified\nFunction in nrepl\n(defn foo ...)";
"Evaluate Modified\nFunction in nrepl\n(defn foo ...)" -> "Call Function\nwith Test Args\n(foo ...)";
"Call Function\nwith Test Args\n(foo ...)" -> "Inspect Captured\nValue at REPL\n(var / keys / type)";
"Inspect Captured\nValue at REPL\n(var / keys / type)" -> "Form Hypothesis\n(Why is this\nwrong?)";
"Form Hypothesis\n(Why is this\nwrong?)" -> "Test Corrected Code\nin nrepl\n(defn foo ...)";
"Test Corrected Code\nin nrepl\n(defn foo ...)" -> "Verify Fix Works\n(Call & verify\nresults)";
"Verify Fix Works\n(Call & verify\nresults)" -> "Update Source File\n(Remove inline def,\napply fix)";
"Update Source File\n(Remove inline def,\napply fix)" -> "Run Test Suite\n(Confirm fix\nsolves problem)";
}
使用 Integrant 组件
如果代码使用了 Integrant 组件,请注意:**修改组件代码并在 nrepl 中求值后,正在运行的组件可能不会应用更改。**即使新代码已加载,你看到的也可能仍是旧行为。
何时重置组件
使用以下决策树:
digraph integrant_decision {
rankdir=TB;
node [shape=diamond, style=rounded];
start [label="Modified code that\nthe component\nuses?", shape=diamond];
no_reset [label="No reset needed\n(Only inspecting\nwith def or\ntesting pure\nfunctions)", shape=box, style=rounded];
do_reset [label="Reset component\nto pick up\nchanges", shape=box, style=rounded];
start -> no_reset [label="No"];
start -> do_reset [label="Yes"];
}
重置方法:
;; After evaluating your code change in nrepl:
(require '[your.component :as comp] :reload)
;; Reset the component to pick up the new code:
(ig/halt! @system :component-key)
(alter-var-root #'@system (fn [s] (ig/init (:system/config @system) :component-key)))
;; Now test again — the component reflects your change
快速检查清单
- 我是否阅读了错误消息和堆栈跟踪?
- 我是否确定了要检查的函数和参数?
- 我是否在 nrepl 中编写了带有
(def var var)的函数? - 我是否求值了修改后的函数定义?
- 我是否调用了函数以触发捕获?
- 我是否检查了捕获的变量(键、类型、值)?
- 我是否针对问题所在提出了假设?
- 我是否先在 nrepl 中测试了修正后的代码?
- 我是否在修改源文件前确认修复有效?
- 我是否在提交前移除了
def行? - 我是否考虑了组件生命周期(Integrant 状态)?