Report binary and merge scan limits
ober
788067d69c71f51766c18c540977bfadd7debc65
--- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ JERBOA ?= jerboa BIN := bin/jerboa-aigit +DIST_BIN := dist/jerboa-aigit -.PHONY: all help test verify install clean +.PHONY: all help test verify binary install clean all: help @@ -10,7 +11,8 @@ help: @echo "" @echo " make test Run fixture smoke tests" @echo " make verify Run syntax smoke plus fixture tests" - @echo " make install Install wrapper to ~/.local/bin/sniff-ai" + @echo " make binary Build native binary into dist/jerboa-aigit" + @echo " make install Install wrapper to ~/.local/bin/jerboa-aigit" @echo " make clean Remove local build artifacts" $(BIN): bin/jerboa-aigit @@ -22,6 +24,11 @@ verify: @$(JERBOA) main-binary.ss --help >/dev/null @tests/fixture-smoke.sh +binary: + @mkdir -p dist + @$(JERBOA) jerbuild binary main-binary.ss $(DIST_BIN) + @$(DIST_BIN) --help >/dev/null + install: @mkdir -p "$(HOME)/.local/bin" @cp bin/jerboa-aigit "$(HOME)/.local/bin/jerboa-aigit" @@ -30,4 +37,3 @@ install: clean: @rm -rf dist tmp - --- a/README.md +++ b/README.md @@ -53,6 +53,10 @@ Resource limits default to 500 changed files per commit, 20,000 added lines analyzed per commit, and 50,000 bytes per AI note. When a limit is hit, output keeps the bounded data and includes a warning. +Warnings also call out root commits with no parent baseline, merge commits where +the scanner intentionally uses the first-parent diff, and binary file changes +that are skipped by text-line heuristics. + ## What It Reads The scanner runs Git commands against the requested repository using fixed @@ -97,6 +101,7 @@ disciplined commit conventions, bot commits, and large refactors. ```bash make test make verify +make binary ``` All Jerboa source is in `main-binary.ss`. Per `AGENTS.md`, edit `.ss` files only --- a/main-binary.ss +++ b/main-binary.ss @@ -177,8 +177,20 @@ (def (parent-time repo parent) (if (string-empty? parent) 0 (parse-int (string-trim (git repo (list "show" "-s" "--format=%ct" parent))) 0))) +(def (parent-list parents) + (filter (lambda (x) (not (blank? x))) (string-split parents #\space))) + +(def (parent-count parents) + (length (parent-list parents))) + +(def (root-commit? parents) + (= (parent-count parents) 0)) + +(def (merge-commit? parents) + (> (parent-count parents) 1)) + (def (first-parent parents) - (let ([parts (filter (lambda (x) (not (blank? x))) (string-split parents #\space))]) + (let ([parts (parent-list parents)]) (if (pair? parts) (car parts) ""))) (def (parse-numstat line) @@ -187,22 +199,39 @@ [dels (parse-int (safe-ref parts 1 "0") 0)] [path (safe-ref parts 2 "")]) (list path adds dels))) +(def (numstat-lines repo rev file) + (let ([args (append (list "show" "--format=" "--numstat" "--first-parent" rev) + (pathspec-args file))]) + (filter (lambda (line) (not (blank? line))) + (split-lines (git repo args))))) + +(def (numstat-line-path line) + (safe-ref (split-tabs line) 2 "")) + +(def (selected-numstat-lines lines explicit-file) + (if explicit-file + lines + (filter (lambda (line) (not (excluded-path? (numstat-line-path line)))) lines))) + +(def (binary-numstat-line? line) + (let ([parts (split-tabs line)]) + (and (>= (length parts) 3) + (same-public-string? (safe-ref parts 0 "") "-") + (same-public-string? (safe-ref parts 1 "") "-")))) + +(def (text-numstat-line? line) + (not (binary-numstat-line? line))) + +(def (binary-file-count repo rev file) + (count-where binary-numstat-line? (selected-numstat-lines (numstat-lines repo rev file) file))) (def (changed-files repo rev file) - (let* ([args (append (list "show" "--format=" "--numstat" "--first-parent" rev) - (pathspec-args file))] - [records (map parse-numstat - (filter (lambda (line) (not (blank? line))) - (split-lines (git repo args))))]) + (let* ([lines (numstat-lines repo rev file)] + [text-lines (filter text-numstat-line? lines)] + [records (map parse-numstat text-lines)]) (selected-file-records records file))) (def (changed-file-count repo rev file) - (let* ([args (append (list "show" "--format=" "--numstat" "--first-parent" rev) - (pathspec-args file))] - [records (map parse-numstat - (filter (lambda (line) (not (blank? line))) - (split-lines (git repo args))))] - [selected (if file records (filter (lambda (record) (not (excluded-path? (car record)))) records))]) - (length selected))) + (length (selected-numstat-lines (numstat-lines repo rev file) file))) (def (numstat-adds files) (sum (map cadr files))) (def (numstat-dels files) (sum (map caddr files))) @@ -468,8 +497,19 @@ [t (parse-int (safe-ref fields 4 "0") 0)]) (if (same-public-string? e author-id) (loop (cdr xs) (cons t out)) (loop (cdr xs) out)))))) -(def (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count) - (append (if (null? files) '("no changed text files found or commit is unavailable") '()) +(def (commit-shape-warnings parents) + (append (if (root-commit? parents) '("root commit; no parent diff baseline") '()) + (if (merge-commit? parents) '("merge commit; scanner uses first-parent diff") '()))) + +(def (binary-change-warnings binary-count) + (if (> binary-count 0) + (list (str "binary file changes skipped: " binary-count)) + '())) + +(def (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count parents binary-count) + (append (commit-shape-warnings parents) + (binary-change-warnings binary-count) + (if (null? files) '("no changed text files found or commit is unavailable") '()) (if (null? lines) '("no added UTF-8 patch lines available") '()) (limit-warning "files" file-count (scan-config-max-files current-config)) (limit-warning "added lines" line-count (scan-config-max-added-lines current-config)) @@ -487,6 +527,7 @@ [author-name (safe-ref fields 2 "")] [author-email (safe-ref fields 3 "")] [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)] [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)] @@ -508,7 +549,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)) + (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count parents binary-count)) (cadr sim-pair)))) (def (scan-repo repo revs file min-lines metadata-only? heuristics-only?) --- a/tests/fixture-smoke.sh +++ b/tests/fixture-smoke.sh @@ -77,6 +77,9 @@ test "$jsonl_lines" = 2 head_rev=$(git -C "$fixture" rev-parse HEAD) base_rev=$(git -C "$fixture" rev-parse HEAD~1) +root_json=$("$root/bin/jerboa-aigit" scan "$fixture" --format json --commit "$base_rev") +printf '%s\n' "$root_json" | grep -q 'root commit; no parent diff baseline' + explain=$("$root/bin/jerboa-aigit" explain "$head_rev" "$fixture") printf '%s\n' "$explain" | grep -q "commit: $head_rev" printf '%s\n' "$explain" | grep -q 'signals:' @@ -158,6 +161,42 @@ 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' +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 -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" 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") +printf '%s\n' "$merge_json" | grep -q 'merge commit; scanner uses first-parent diff' + +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 -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 -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") +printf '%s\n' "$binary_json" | grep -q 'binary file changes skipped: 1' +printf '%s\n' "$binary_json" | grep -q 'no added UTF-8 patch lines available' + if "$root/bin/jerboa-aigit" scan "$fixture/nope" >/tmp/jerboa-aigit-invalid.out 2>&1; then echo "invalid repository path should fail" >&2 exit 1