ClaudeMods
☰
ZH-CN
● 0 人在线 · 浏览 0 次
赞助提交作品
GitHub 仓库 · 发布者 KilimcininKorOglu

contract-watch

模型使用 Edit 修改函数签名后,将 ripwire 找到的调用方加入 Edit 结果,让模型在构建前修复它们。

KilimcininKorOglu@KilimcininKorOglu

KilimcininKorOglu/claude-code-mods/tree/main/plugins/contract-watch

已翻译

关于这个 mod

contract-watch

模型给函数增加参数,编辑看起来没问题,但代码其他位置的三个调用方已经损坏;直到构建或测试失败时你才发现。这个 mod 在编辑时捕获问题:当 Edit 修改函数参数时,它会询问 ripwire 该函数的调用方,并将调用方放进 Edit 结果。

它做什么

  1. 监视 Edit 工具。成功编辑后,比较 old_string 和 new_string 中的单行函数定义:Go 的 func、JS 和 TS 函数、箭头函数与类方法、Python 的 def、Rust 的 fn、Java 方法以及 PHP 函数。
  2. 两个字符串都定义了函数但参数不同,就是签名变化。只修改函数体的编辑不会运行任何操作。
  3. 对每个变化的签名,通过 argv 运行 ripwire <repo root> --edit-check=<file>:<name>。ripwire 将定义与 git HEAD 比较并列出调用方。
  4. 当 ripwire 报告 status=contract-change 时,模型会在 Edit 结果后读取提示:contract-watch: parse changed from 1 to 2 parameter(s) since the last commit; check each caller: main (main.go:5), other (main.go:9)。最多列出 10 个调用方;incompatible=1 的调用方优先,并会显示 contract-watch: parse changed from 1 to 2 parameter(s) since the last commit; these callers do not match the new arity: main (main.go:5). Other callers of that name, which the call graph binds by name and may belong to another type: other (lib.go:9). Check each。Messaging::sendAlert 和 SNMP_Monitor::sendAlert 这类同名方法不会被丢弃。
  5. 同时在转录中写入一行,只显示发现:contract-watch: parse changed from 1 to 2 parameter(s); do not match: main (main.go:5); same name: other (lib.go:9)。提示和转录行是两个独立通道。
  6. 打开 sidebar 后,发现会进入串流。第一行显示变化,下面显示红色的已标记调用方,以及淡色的同名调用方和 (file:line)。没有 sidebar 时,这一行进入转录。提示会列出每个调用方;Go 实际检查中 ripwire 报告 incompatible=0,但两个调用方仍传入一个参数(用 ripwire 2.1.278 测得)。
  7. mod 会在两种模式下保留发现,并在下一次 git commit、git push 或 git merge 前重新测量。所有调用方匹配时显示 contract-watch: every caller matches parse again;契约仍不同但没有标记时显示 contract-watch: no caller of parse carries the mismatch mark any more。测量发生在命令前,提交后仍使用旧参数的调用方会让发现保持打开。
  8. 每个主循环回合结束时,未关闭发现会再次测量,并在下一次提示中传给模型:contract-watch: 1 changed signature(s) still leave a caller behind: parse changed from 1 to 2 parameter(s), 1 caller(s) do not match. Bring each caller to the new signature, or take the signature change back。
  9. deny 模式会在 changed signature 仍有调用方时停止命令。只有 incompatible 数量大于 0 的检查会阻止操作。git commit 读取索引(每个储存库一次执行 git diff --cached --name-only -z),push 和 merge 则计算全部发现。没有绕过方式;用 /contract-watch mode note 关闭门禁,note 是默认模式。

Command

/contract-watch                 on or off, the mode, and the signatures that leave a caller behind
/contract-watch on | off        on by default
/contract-watch mode note       note only; the default
/contract-watch mode deny       a commit, a push and a merge also stop while a caller does not match

Install

claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install contract-watch@kilimcininkoroglu-mods

Function hooks are early access. Claude Code 2.1.288 and later load them by default。

After installing

  1. 安装 ripwire 并放到 PATH。没有它时,会写入 the callers were not checked: ...,编辑仍会继续。
  2. 重启 Claude Code。

What it can reach

Validated with claude plugin validate on Claude Code 2.1.283:

./register.ts hooks: session.start, command.run{command=contract-watch}, turn.complete, prompt.submit, tool.call{tool=Bash}, tool.call{tool=Edit}
./register.ts calls: $.command.register, $.process.run (via askRipwire, locate, stagedIn), $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get (via isEnabled, readSettings), $.store.set (via runCommand, setMode), $.ui.log (via atGitCommand, toPerson)

Reach L2:它会运行进程。

1. Reads:    the old and new text of each Edit; the Bash command text; through ripwire, the repository's source and git HEAD
2. Runs:     git rev-parse, git diff --cached --name-only -z and ripwire --edit-check, read-only, by argv, after an edit that changed a signature, and once per open symbol before a git commit, push or merge and at each turn's end
3. Sends:    a note to the model after the Edit's result, one more with the next prompt while a finding stands, and one line to the transcript; nothing leaves the machine
4. Persists: in $.store, the on/off setting and the mode
5. Hostile input: a function name comes from the edited text and reaches ripwire as one argv item, never through a shell

Limits

  • 只读取单行定义;跨多行的参数签名看不到。
  • 重命名函数不会检查,因为旧名称已不存在。
  • 比较对象是 git HEAD;提交前再次编辑同一函数会重复提示。
  • 只监视 Edit,替换整个文件的 Write 不会触发。
  • 在 git 储存库外不运行。
  • ripwire 不索引的函数不会检查,例如 PHP 的 <script> 块中的 JavaScript 函数。
  • 门禁遵循 ripwire 的 incompatible 数量;按名称无法绑定的调用方不会阻止门禁。
  • deny 模式没有绕过方式;通过脚本或隐藏 git commit 的别名执行提交不会被停止。
  • git commit -a、-am 和 -- 后带路径规格的提交不会缩小到索引,所有未关闭发现都会计入。

Development

make install     # eslint, typescript-eslint, typescript
make lint        # complexity limit 10, the build fails above it
make typecheck   # needs .claude/types/ from /plugin-types
make validate
make test        # claude plugin test

安装

请先查看作者 README 确认 marketplace 和插件名称;命令可能随仓库结构改变。

claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install contract-watch
原文 / README

contract-watch

The model adds a parameter to a function, the edit looks fine, and three callers elsewhere in the code are now broken; you find out when the build or a test fails. This mod catches it at the edit: when an Edit changes a function's parameters, it asks ripwire who calls that function and puts the callers right into the Edit's result.

What it does

  1. It watches the Edit tool. After a successful edit it compares the one-line function definitions in old_string and new_string: Go func, JS and TS functions, arrow functions and class methods, Python def, Rust fn, Java methods and PHP functions.

  2. A function that both strings define with different parameters is a changed signature. An edit that only touches a body runs nothing.

  3. For each changed signature it runs ripwire <repo root> --edit-check=<file>:<name> by argv. ripwire compares the definition with git HEAD and lists the callers.

  4. When ripwire reports status="contract-change", the model reads this note right after the Edit's result:

    contract-watch: parse changed from 1 to 2 parameter(s) since the last commit; check each caller: main (main.go:5), other (main.go:9).
    

    Each caller is named with the definition it sits in; at most 10 are named and the rest are counted. When ripwire marks a caller incompatible="1", every folded definition it sees disagrees with the new arity. Those callers come first, under a sentence of their own:

    contract-watch: parse changed from 1 to 2 parameter(s) since the last commit; these callers do not match the new arity: main (main.go:5). Other callers of that name, which the call graph binds by name and may belong to another type: other (lib.go:9). Check each.
    

    The second group matters in a codebase where several types define a method with the same name: the call graph binds a call by its name, so Messaging::sendAlert reads the same as SNMP_Monitor::sendAlert. Neither group is dropped.

  5. At the same moment you get one line in the transcript, so you see what the model was told. It holds the finding alone, without the instruction:

    contract-watch: parse changed from 1 to 2 parameter(s); do not match: main (main.go:5); same name: other (lib.go:9)
    

    The note and the line are separate channels: the model never reads the line, and you never read the note.

  6. With the sidebar open, the finding goes into its stream instead and the transcript stays clean. The first line shows the change (the old parameter count faint, the new one yellow). Under it come the marked callers' names in red, then the same-named ones in faint text after a same name, may be another type line, each with its (file:line) faint. The entry stays until newer ones push it off the pane. Without the sidebar, the line lands in the transcript as above.

The note lists every caller, not only the ones ripwire proves incompatible: in a live check on Go, ripwire reported incompatible="0" while both callers still passed one argument (measured with ripwire on 2.1.278).

  1. The mod holds every reported signature open and closes it itself, in both modes. At the next git commit, git push or git merge the model runs, and before that command runs, ripwire measures each open symbol again. A symbol that no caller misses any more closes with a green line, and its sidebar entry is dropped:

    contract-watch: every caller matches parse again
    

    That line comes when the contract reads the same as the last commit again. When the contract still differs but no caller carries ripwire's incompatible mark any more, the closing line names that narrower measure instead, because a call graph that binds by name cannot prove every caller right:

    contract-watch: no caller of parse carries the mismatch mark any more
    

    The measurement runs before the command, not after it. --edit-check compares the working tree against git HEAD, so once a commit has landed there is nothing left to compare and every finding would look closed. For the same reason an open symbol is held by ripwire's incompatible mark alone, not by the contract status: after a commit took the change, the contract reads as HEAD, while a caller left on the old arity still carries the mark, so the finding stays open at the turn's end until the mark is gone.

  2. A finding the model did not close is measured the same way at the end of each main-loop turn, and whatever is left reaches the model as one note with your next prompt. ripwire runs on this machine, once per open symbol:

    contract-watch: 1 changed signature(s) still leave a caller behind: parse changed from 1 to 2 parameter(s), 1 caller(s) do not match. Bring each caller to the new signature, or take the signature change back.
    

    That is one note per turn, not one per prompt. Without it the finding would be said once, at the edit, and then sit in the pane while the model forgot about it. You read nothing new, because the pane already shows the same finding.

  3. In deny mode that same moment also stops the command while a changed signature leaves a caller behind. The gate uses a narrower measure than the note: only a check whose incompatible count is above zero holds it, meaning the callers ripwire names on fixed-arity evidence. A git commit answers for its own files alone: the mod reads the index (git diff --cached --name-only -z, once per repository) and lets the commit run when it holds none of the files those signatures live in, with one line telling you how many still stand. A push and a merge have no index to read, so every finding counts there. There is no bypass; only you turn the gate off, with /contract-watch mode note. note mode is the default and stops nothing.

In the live check the model read the note after its Edit and said that the two callers would not compile until they were updated.

Command

/contract-watch                 on or off, the mode, and the signatures that leave a caller behind
/contract-watch on | off        on by default
/contract-watch mode note       note only; the default
/contract-watch mode deny       a commit, a push and a merge also stop while a caller does not match

Install

claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install contract-watch@kilimcininkoroglu-mods

Function hooks are early access. Claude Code 2.1.288 and later load them by default, so there is nothing to switch on.

After installing

  1. Install ripwire and put it on PATH. Without it, a changed signature writes the callers were not checked: ... as a yellow entry (a transcript line with the sidebar closed), once until a different error comes, and the edit goes through as before.
  2. Restart Claude Code.

What it can reach

Validated with claude plugin validate on Claude Code 2.1.283:

❯ ./register.ts hooks: session.start, command.run{command=contract-watch}, turn.complete, prompt.submit, tool.call{tool=Bash}, tool.call{tool=Edit}
❯ ./register.ts calls: $.command.register, $.process.run (via askRipwire, locate, stagedIn), $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get (via isEnabled, readSettings), $.store.set (via runCommand, setMode), $.ui.log (via atGitCommand, toPerson)

Reach L2: it runs processes.

1. Reads:    the old and new text of each Edit; the Bash command text; through ripwire, the repository's source and git HEAD
2. Runs:     git rev-parse, git diff --cached --name-only -z and ripwire --edit-check, read-only, by argv, after an edit that changed a signature, and once per open symbol before a git commit, push or merge and at each turn's end
3. Sends:    a note to the model after the Edit's result, one more with the next prompt while a finding stands, and one line to the transcript; nothing leaves the machine
4. Persists: in $.store, the on/off setting and the mode
5. Hostile input: a function name comes from the edited text and reaches ripwire as one argv item, never through a shell

Limits

  • Only a definition on one line is read. A signature whose parameters span several lines is not seen.
  • A renamed function is not checked: the old name is gone, so ripwire has nothing to compare.
  • The comparison is against git HEAD. A second signature edit of the same function before a commit repeats the note.
  • Only the Edit tool is watched. A Write that replaces a whole file is not.
  • Outside a git repository nothing runs.
  • A function ripwire does not index, such as a JavaScript function inside a PHP file's <script> block, is not checked. The sidebar shows one faint line, <name>: ripwire does not index it, its callers were not checked, and the model reads nothing. An open symbol ripwire no longer indexes was removed or renamed, and its finding closes.
  • The gate follows ripwire's incompatible count, which is itself a floor: a caller ripwire cannot bind by name does not hold the gate. The note remains the wider measure.
  • The deny mode has no bypass. When a finding cannot be fixed, you turn the gate off with /contract-watch mode note.
  • The gate reads the command text. A commit through a script or an alias that hides git commit is not stopped; the finding is then measured at the next turn's end instead.
  • A git commit -a, a -am and a commit with a pathspec after -- are not narrowed to the index, because they commit files the index does not hold yet. Every open finding counts for those.

Development

make install     # eslint, typescript-eslint, typescript
make lint        # complexity limit 10, the build fails above it
make typecheck   # needs .claude/types/ from /plugin-types
make validate
make test        # claude plugin test

更多类似作品