L2 v0.8.0 source

contract-watch

Model bir fonksiyon signature'ını değiştirdiğinde hangi caller'ların kontrol edilmesi gerektiğini modele söyleyen bir Claude Code Mod'u. Bir Edit bir fonksiyonun parametrelerini değiştirdiğinde mod, ripwire ile onu kimin çağırdığını sorar ve caller'ları Edit'in sonucuna ekler, build ya da test onları bulmadan önce.

Ne yapar

  1. Mod Edit tool'unu hook'lar. Başarılı bir edit'ten sonra old_string ve new_string içindeki tek satırlık fonksiyon tanımlarını karşılaştırır: Go func, JS ve TS fonksiyonları, arrow function'lar ve class method'ları, Python def, Rust fn, Java method'ları ve PHP fonksiyonları.

  2. İki metnin de farklı parametrelerle tanımladığı bir fonksiyon, değişmiş bir signature'dır. Gövde edit'i hiçbir şey çalıştırmaz.

  3. Değişen her signature için ripwire <repo root> --edit-check=<file>:<name> komutunu argv ile çalıştırır. ripwire tanımı git HEAD ile karşılaştırır ve caller'ları listeler.

  4. ripwire status="contract-change" raporladığında model, Edit'in sonucundan sonra şu notu okur:

    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).
    

    Bir caller, içinde durduğu tanımla adlandırılır; en fazla 10 tanesi adlandırılır, gerisi sayılır. ripwire bir caller'ı incompatible="1" olarak işaretlediğinde, gördüğü her tanım yeni arity ile uyuşmuyor demektir ve o caller'lar kendi cümleleri altında önce gelir:

    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.
    

    İkinci grup, birden çok tipin aynı adda bir method tanımladığı bir kod tabanında önemlidir: call graph bir çağrıyı adına göre bağlar, yani Messaging::sendAlert ile SNMP_Monitor::sendAlert aynı okunur. İki grup da atılmaz.

  5. Aynı anda transcript'e bir satır yazılır, böylece modele ne söylendiğini görürsünüz. Bu satır talimat cümlesi olmadan yalnız bulguyu taşır:

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

    Not ve satır ayrı iki kanaldır: model satırı hiç okumaz, siz notu hiç okumazsınız.

  6. sidebar açıkken bulgu oraya gider: ilk satırda değişim, altında kırmızı ile işaretli caller'lar ve bir same name, may be another type satırından sonra soluk renkte aynı adlı olanlar, stream'in içinde bir kayıt olarak; transcript temiz kalır. Kayıt, yenileri onu pane'in dışına itene kadar durur. Sidebar kapalıyken ya da o mod kurulu değilken yukarıdaki transcript satırı yazılır.

Not, yalnız ripwire'ın uyumsuz kanıtladıklarını değil, her caller'ı listeler: Go üzerindeki canlı bir testte iki caller da hâlâ tek argüman geçerken ripwire incompatible="0" raporladı (ripwire ile 2.1.278 üzerinde ölçüldü).

  1. Mod raporladığı her signature'ı açık tutar ve iki modda da kendisi kapatır. Modelin çalıştırdığı bir sonraki git commit, git push ya da git merge anında, o komut çalışmadan önce, ripwire her açık sembolü tekrar ölçer. Artık hiçbir caller'ın kaçırmadığı bir sembol yeşil bir satırla kapanır ve bulgunun sidebar kaydı düşer:

    contract-watch: every caller matches parse again
    

    Bu satır, contract son commit ile yeniden aynı okunduğunda gelir. Contract hâlâ farklıyken hiçbir caller ripwire'ın incompatible işaretini taşımıyorsa, kapanış satırı onun yerine bu daha dar ölçümü adlandırır, çünkü adına göre bağlayan bir call graph her caller'ın doğru olduğunu kanıtlayamaz:

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

    Ölçüm komuttan sonra değil, önce çalışır: --edit-check working tree'yi git HEAD ile karşılaştırır, yani commit düştükten sonra karşılaştıracak bir şey kalmaz ve her bulgu kapanmış okunurdu.

  2. Modelin kapatmadığı bir bulgu her main-loop turn sonunda aynı şekilde ölçülür ve geriye kalan, bir sonraki prompt ile modele tek not olarak ulaşır. ripwire bu makinede, açık sembol başına bir kere çalışır:

    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.
    

    Turn başına bir not, prompt başına değil. Bu olmasa bulgu bir kere, edit anında söylenir ve model onu unutmuşken pane'de dururdu. Siz yeni bir şey okumazsınız: pane zaten aynı bulguyu taşır.

  3. deny modunda aynı an komutu da durdurur, değişmiş bir signature bir caller'ı geride bıraktığı sürece. Gate nottan daha dar bir ölçüm alır: yalnız incompatible sayısı sıfırdan büyük olan bir kontrol gate'i tutar, yani ripwire'ın sabit arity kanıtıyla adlandırdığı caller'lar. Bir git commit yalnız kendi dosyalarından sorumludur: mod index'i okur (git diff --cached --name-only, repository başına bir kere) ve commit o signature'ların yaşadığı dosyalardan hiçbirini tutmuyorsa çalışmasına izin verir, kaç bulgunun durduğunu söyleyen bir satırla. push ve merge hiçbir index okumaz, bu yüzden orada her bulgu durur. Kaçış yolu yoktur; gate'i yalnız kişi /contract-watch mode note ile kapatır. note varsayılandır ve hiçbir şeyi durdurmaz.

Canlı testte model Edit'inden sonra notu okudu ve iki caller'ın güncellenmeden derlenmeyeceğini söyledi.

Komut

/contract-watch                 on ya da off, mod ve caller geride bırakan signature'lar
/contract-watch on | off        varsayılan on
/contract-watch mode note       sadece not; varsayılan
/contract-watch mode deny       bir caller uyuşmuyorken commit, push ve merge de durur

Kurulum

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

Function hook'lar early access. Flag olmadan hiçbir şey yüklenmez. Flag'i kalıcı yapmak için ~/.claude/settings.json dosyasına ekleyin:

{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }

Kurulumdan sonra

  1. ripwire kurun ve PATH'e koyun. Olmadan her değişen signature bir kere the callers were not checked: ... satırını yazar ve edit eskisi gibi çalışır.
  2. Claude Code'u yeniden başlatın.

Nereye uzanır

Claude Code 2.1.278 üzerinde claude plugin validate ile doğrulandı:

❯ ./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, $.store.set (via runCommand, setMode), $.ui.log (via atGitCommand, report, toPerson)

Reach L2, process çalıştırır.

1. Okur:     her Edit'in eski ve yeni metnini; Bash komut metnini; ripwire üzerinden repository'nin kaynak kodunu ve git HEAD'ini
2. Çalıştırır: git rev-parse, git diff --cached --name-only ve ripwire --edit-check, salt okuma, argv ile: signature değiştiren bir edit'ten sonra, bir git commit, push ya da merge öncesinde ve her turn sonunda açık sembol başına bir kere
3. Gönderir: Edit'in sonucundan sonra modele bir not, bulgu dururken bir sonraki prompt ile bir not daha, ve transcript'e bir satır; makineden hiçbir şey çıkmaz
4. Saklar:   $.store içinde on/off ayarını ve modu
5. Düşman girdi: fonksiyon adı düzenlenen metinden gelir ve ripwire'a tek bir argv değeri olarak ulaşır, hiçbir zaman shell üzerinden geçmez

Sınırlar

  • Yalnız tek satırdaki bir tanım okunur. Parametreleri birkaç satıra yayılan bir signature görülmez.
  • Yeniden adlandırılmış bir fonksiyon kontrol edilmez: eski ad gitmiştir, yani ripwire'ın karşılaştıracak bir şeyi yoktur.
  • Karşılaştırma git HEAD'e karşıdır. Commit'ten önce aynı fonksiyonun ikinci bir signature edit'i notu tekrarlar.
  • Yalnız Edit tool'u izlenir. Tüm dosyayı değiştiren bir Write izlenmez.
  • git repository'si dışında hiçbir şey çalışmaz.
  • Gate, ripwire'ın incompatible sayısını izler; o sayı kendisi bir alt sınırdır: ripwire'ın adına göre bağlayamadığı bir caller gate'i tutmaz. Not daha geniş ölçüm olarak kalır.
  • deny modunun kaçış yolu yoktur. Bulgu düzeltilemiyorsa kişi gate'i /contract-watch mode note ile kapatır.
  • Gate komut metnini okur. git commit komutunu gizleyen bir script ya da alias üzerinden atılan commit durdurulmaz; bulgu o zaman bir sonraki turn sonunda ölçülür.
  • git commit -a, -am ve -- sonrası pathspec taşıyan bir commit index'e göre daraltılmaz, çünkü bunlar index'in henüz tutmadığı dosyaları commit eder. Onlar için her açık bulgu durur.

Geliştirme

make install     # eslint, typescript-eslint, typescript
make lint        # complexity limiti 10, üstünde build'i düşürür
make typecheck   # /plugin-types ile üretilen .claude/types/ gerekir
make validate
make test        # claude plugin test

contract-watch

A Claude Code Mod that tells the model which callers to check after it changes a function signature. When an Edit changes the parameters of a function, the mod asks ripwire who calls it and adds the callers to the Edit's result, before a build or a test finds them.

What it does

  1. The mod hooks 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 both strings define with other parameters is a changed signature. A body edit 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 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).
    

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

    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 of one 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. The same moment writes one line to the transcript, so you see what the model was told. The line 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. While the sidebar is open, that finding goes there instead, the change on the first line, the marked callers in red under it and the same-named ones faint after a same name, may be another type line, as an entry in its stream, and the transcript stays clean. The entry stays until newer ones push it off the pane. With the sidebar closed, or without that mod installed, the transcript line is written 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 no caller misses any more closes with a green line, and the sidebar entry of the finding 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 read as closed.

  2. A finding the model did not close is measured the same way at the end of each main-loop turn, and what is left reaches the model as one note with its 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.
    

    One note per turn, not one per prompt. Without this the finding would be said once, at the edit, and then stand in the pane while the model forgot it. You read nothing new: the pane already carries the same finding.

  3. In deny mode that same moment also stops the command while a changed signature leaves a caller behind. The gate takes a narrower measure than the note: only a check whose incompatible count is above zero holds it, the callers ripwire names by fixed-arity evidence. A git commit answers for its own files alone: the mod reads the index (git diff --cached --name-only, once per repository) and lets the commit run when it holds none of the files those signatures live in, with one line to you naming how many still stand. A push and a merge hold no index to read, so every finding stands there. There is no bypass; only the person turns 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. Nothing loads without the flag. To keep it on, add this to ~/.claude/settings.json:

{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }

After installing

  1. Install ripwire and put it on PATH. Without it every changed signature logs the callers were not checked: ... once, and the edit runs as before.
  2. Restart Claude Code.

What it can reach

Validated with claude plugin validate on Claude Code 2.1.278:

❯ ./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, $.store.set (via runCommand, setMode), $.ui.log (via atGitCommand, report, toPerson)

Reach L2, 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 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.
  • 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 stays the wider measure.
  • The deny mode has no bypass. When a finding cannot be fixed, the person turns 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, and 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 stands for those.

Development

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