Warn on rename and copy diffs

ober

4bdc73e7a2e5ce414fb6e9f7c641e61454b60324

diff --git a/README.md b/README.md
index b584023..187da7e 100644
--- a/README.md
+++ b/README.md
@@ -86,8 +86,8 @@ uses median/MAD-style addition-count deviation across the selected scan window.
 
 Warnings also call out root commits with no parent baseline, merge commits where
 the scanner intentionally uses the first-parent diff, missing parent objects in
-shallow history, and binary file changes that are skipped by text-line
-heuristics.
+shallow history, rename/copy changes, and binary file changes that are skipped
+by text-line heuristics.
 
 ## What It Reads
 
diff --git a/main-binary.ss b/main-binary.ss
index 398c71e..ca5f020 100644
--- a/main-binary.ss
+++ b/main-binary.ss
@@ -292,6 +292,29 @@
 
 (def (binary-file-count repo rev file)
   (count-where binary-numstat-line? (selected-numstat-lines (numstat-lines repo rev file) file)))
+(def (status-lines repo rev file)
+  (let ([args (append (git-show-diff-args rev '("--name-status" "--find-renames" "--find-copies"))
+                      (pathspec-args file))])
+    (filter (lambda (line) (not (blank? line)))
+            (split-lines (git repo args)))))
+
+(def (status-line-path line)
+  (let ([parts (split-tabs line)])
+    (if (>= (length parts) 3)
+        (safe-ref parts 2 "")
+        (safe-ref parts 1 ""))))
+
+(def (selected-status-lines lines explicit-file)
+  (if explicit-file
+      lines
+      (filter (lambda (line) (not (excluded-path? (status-line-path line)))) lines)))
+
+(def (rename-copy-status-line? line)
+  (let ([status (safe-ref (split-tabs line) 0 "")])
+    (or (string-prefix? "R" status) (string-prefix? "C" status))))
+
+(def (rename-copy-change-count repo rev file)
+  (count-where rename-copy-status-line? (selected-status-lines (status-lines repo rev file) file)))
 
 (def (changed-files repo rev file)
   (let* ([lines (numstat-lines repo rev file)]
@@ -634,11 +657,16 @@
   (if (> binary-count 0)
       (list (str "binary file changes skipped: " binary-count))
       '()))
+(def (rename-copy-warnings rename-copy-count)
+  (if (> rename-copy-count 0)
+      (list (str "rename/copy changes detected: " rename-copy-count))
+      '()))
 
-(def (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count parents shallow? binary-count missing-parent?)
+(def (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count parents shallow? binary-count rename-copy-count missing-parent?)
   (append (commit-shape-warnings parents)
           (parent-history-warnings shallow? missing-parent?)
           (binary-change-warnings binary-count)
+          (rename-copy-warnings rename-copy-count)
           (llm-warnings)
           (if (null? files) '("no changed text files found or commit is unavailable") '())
           (if (null? lines) '("no added UTF-8 patch lines available") '())
@@ -661,6 +689,7 @@
          [time (parse-int (safe-ref fields 4 "0") 0)] [subject (safe-ref fields 5 "")]
          [body (commit-message repo rev)] [file-count (changed-file-count repo rev file)]
          [binary-count (binary-file-count repo rev file)]
+         [rename-copy-count (rename-copy-change-count repo rev file)]
          [files (changed-files repo rev file)] [paths (numstat-paths files)]
          [adds (numstat-adds files)] [dels (numstat-dels files)]
          [line-count (added-line-count-for-paths repo rev paths file)]
@@ -683,7 +712,7 @@
                   [heuristics-only? (verdict score '() "")]
                   [else (verdict score metadata note)])])
     (list (make-finding id parent author-name author-email time subject paths adds dels (length lines) note attribution metadata signals score v
-                        (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count parents shallow? binary-count missing-parent?))
+                        (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count parents shallow? binary-count rename-copy-count missing-parent?))
           (cadr sim-pair))))
 
 (def (scan-repo repo revs file min-lines metadata-only? heuristics-only?)
diff --git a/tests/fixture-smoke.sh b/tests/fixture-smoke.sh
index ab02452..d64f886 100755
--- a/tests/fixture-smoke.sh
+++ b/tests/fixture-smoke.sh
@@ -206,22 +206,29 @@ printf '%s\n' "$insufficient" | grep -q '"verdict":"insufficient-evidence"'
 printf '%s\n' "$insufficient" | grep -q '"signals":\[\]'
 printf '%s\n' "$insufficient" | grep -q 'no changed text files found or commit is unavailable'
 
+git -C "$fixture" mv README.md README-renamed.md
+GIT_AUTHOR_DATE='2026-07-29T09:03:00-06:00' \
+GIT_COMMITTER_DATE='2026-07-29T09:03:00-06:00' \
+  git -C "$fixture" commit -q -m 'rename readme'
+rename_json=$("$root/bin/jerboa-aigit" scan "$fixture" --format json --count 1)
+printf '%s\n' "$rename_json" | grep -q 'rename/copy changes detected: 1'
+
 base_branch=$(git -C "$fixture" branch --show-current)
 git -C "$fixture" checkout -q -b feature-branch
 mkdir -p "$fixture/branch"
 printf 'branch work\n' > "$fixture/branch/feature.txt"
 git -C "$fixture" add branch/feature.txt
-GIT_AUTHOR_DATE='2026-07-29T09:03:00-06:00' \
-GIT_COMMITTER_DATE='2026-07-29T09:03:00-06:00' \
+GIT_AUTHOR_DATE='2026-07-29T09:04:00-06:00' \
+GIT_COMMITTER_DATE='2026-07-29T09:04:00-06:00' \
   git -C "$fixture" commit -q -m 'feature branch work'
 git -C "$fixture" checkout -q "$base_branch"
 printf 'main work\n' > "$fixture/src/human.txt"
 git -C "$fixture" add src/human.txt
-GIT_AUTHOR_DATE='2026-07-29T09:04:00-06:00' \
-GIT_COMMITTER_DATE='2026-07-29T09:04:00-06:00' \
-  git -C "$fixture" commit -q -m 'main branch work'
 GIT_AUTHOR_DATE='2026-07-29T09:05:00-06:00' \
 GIT_COMMITTER_DATE='2026-07-29T09:05:00-06:00' \
+  git -C "$fixture" commit -q -m 'main branch work'
+GIT_AUTHOR_DATE='2026-07-29T09:06:00-06:00' \
+GIT_COMMITTER_DATE='2026-07-29T09:06:00-06:00' \
   git -C "$fixture" merge -q --no-ff feature-branch -m 'merge feature branch'
 merge_rev=$(git -C "$fixture" rev-parse HEAD)
 merge_json=$("$root/bin/jerboa-aigit" scan "$fixture" --format json --commit "$merge_rev")
@@ -234,13 +241,13 @@ printf '%s\n' "$all_parent_json" | grep -q 'src/human.txt'
 
 printf '*.bin binary\n' > "$fixture/.gitattributes"
 git -C "$fixture" add .gitattributes
-GIT_AUTHOR_DATE='2026-07-29T09:06:00-06:00' \
-GIT_COMMITTER_DATE='2026-07-29T09:06:00-06:00' \
+GIT_AUTHOR_DATE='2026-07-29T09:07:00-06:00' \
+GIT_COMMITTER_DATE='2026-07-29T09:07:00-06:00' \
   git -C "$fixture" commit -q -m 'mark binary assets'
 printf '\000\001binary\002\n' > "$fixture/model.bin"
 git -C "$fixture" add model.bin
-GIT_AUTHOR_DATE='2026-07-29T09:07:00-06:00' \
-GIT_COMMITTER_DATE='2026-07-29T09:07:00-06:00' \
+GIT_AUTHOR_DATE='2026-07-29T09:08:00-06:00' \
+GIT_COMMITTER_DATE='2026-07-29T09:08:00-06:00' \
   git -C "$fixture" commit -q -m 'add binary model asset'
 binary_rev=$(git -C "$fixture" rev-parse HEAD)
 binary_json=$("$root/bin/jerboa-aigit" scan "$fixture" --format json --commit "$binary_rev")