Batch refs notes lookup for scans
ober
fe43ab6c2b745070413278421f25260bac934d60
--- a/GAPS.md +++ b/GAPS.md @@ -312,6 +312,13 @@ Acceptance criteria: - Preserve read-only behavior. - Benchmark against per-commit lookup on a medium history. +Status: implemented with a selected-commit note map built from one +`git notes --ref=ai list` call, followed by blob reads only for matching +selected commits. Explicit `--commit` revs are normalized to commit object IDs +so the batch index still resolves notes. Added fixture coverage plus +`benchmarks/note-lookup.sh`; a 60-commit local run measured 1s for per-commit +`git notes show` probing vs 0s rounded for the batched index path. + ### G-032: Recovered attribution is too shallow `git-ai` can recover attribution from known agent identities, file timestamps, new file mode 100755 --- /dev/null +++ b/benchmarks/note-lookup.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env sh +set -eu + +commits="${1:-150}" +note_every="${2:-3}" +fixture=$(mktemp -d) +trap 'rm -rf "$fixture"' EXIT + +git -C "$fixture" init -q +git -C "$fixture" config user.name "Bench User" +git -C "$fixture" config user.email "bench@example.test" + +i=1 +while [ "$i" -le "$commits" ]; do + printf 'line %s\n' "$i" >> "$fixture/file.txt" + git -C "$fixture" add file.txt + GIT_AUTHOR_DATE="2026-07-29T10:00:00-06:00" \ + GIT_COMMITTER_DATE="2026-07-29T10:00:00-06:00" \ + git -C "$fixture" commit -q -m "commit $i" + if [ $((i % note_every)) -eq 0 ]; then + git -C "$fixture" notes --ref=ai add -m '{"tool":"codex","model":"gpt-5","lines":[{"path":"file.txt","start":1,"end":1}]}' HEAD + fi + i=$((i + 1)) +done + +revs_file="$fixture/revs.txt" +git -C "$fixture" rev-list --max-count="$commits" HEAD > "$revs_file" + +per_commit_start=$(date +%s) +while IFS= read -r rev; do + git -C "$fixture" notes --ref=ai show "$rev" >/dev/null 2>/dev/null || true +done < "$revs_file" +per_commit_end=$(date +%s) + +batch_start=$(date +%s) +notes_file="$fixture/notes.txt" +git -C "$fixture" notes --ref=ai list > "$notes_file" +while IFS= read -r rev; do + blob=$(awk -v rev="$rev" '$2 == rev { print $1; exit }' "$notes_file") + if [ -n "$blob" ]; then + git -C "$fixture" cat-file -p "$blob" >/dev/null + fi +done < "$revs_file" +batch_end=$(date +%s) + +printf 'commits=%s\n' "$commits" +printf 'note_every=%s\n' "$note_every" +printf 'per_commit_notes_show_seconds=%s\n' "$((per_commit_end - per_commit_start))" +printf 'batched_notes_list_seconds=%s\n' "$((batch_end - batch_start))" --- a/main-binary.ss +++ b/main-binary.ss @@ -192,7 +192,8 @@ (def (commit-list repo count commit from to file) (if commit - (list commit) + (let ([resolved (string-trim (git repo (list "rev-parse" (str commit "^{commit}"))))]) + (if (string-empty? resolved) (list commit) (list resolved))) (let* ([base (append (list "rev-list") (count-args count) (list (range-spec from to)))] [args (append base (pathspec-args file))]) (filter (lambda (line) (not (blank? line))) @@ -205,6 +206,30 @@ (def (commit-message repo rev) (git repo (list "show" "-s" "--format=%B" rev))) (def (note-text repo rev) (string-trim (git repo (list "notes" "--ref=ai" "show" rev)))) +(def (note-index repo) + (let ([ht (make-hash-table)]) + (for ([line (split-lines (git repo '("notes" "--ref=ai" "list")))]) + (let ([parts (filter (lambda (part) (not (blank? part))) (string-split line #\space))]) + (if (>= (length parts) 2) + (hash-put! ht (safe-ref parts 1 "") (safe-ref parts 0 ""))))) + ht)) + +(def (note-blob-text repo blob) + (if (string-empty? blob) + "" + (string-trim (git repo (list "cat-file" "-p" blob))))) + +(def (selected-note-map repo revs) + (let ([idx (note-index repo)] + [ht (make-hash-table)]) + (for ([rev revs]) + (let ([blob (hash-ref idx rev "")]) + (if (not (string-empty? blob)) + (hash-put! ht rev (note-blob-text repo blob))))) + ht)) + +(def (note-text-from-map note-map rev) + (hash-get/default note-map rev "")) (def (bounded-note-text repo rev) (bounded-string (note-text repo rev) (scan-config-max-note-bytes current-config))) @@ -1213,7 +1238,7 @@ (if (and (string-empty? note) (not metadata-only?)) '("no refs/notes/ai authorship note found") '()) (if (and metadata-only? heuristics-only?) '("metadata-only and heuristics-only were both requested") '()))) -(def (scan-one repo rev hashes revs file min-lines metadata-only? heuristics-only?) +(def (scan-one repo rev note-map hashes revs file min-lines metadata-only? heuristics-only?) (let* ([fields (commit-fields repo rev)] [id (safe-ref fields 0 rev)] [parents (safe-ref fields 1 "")] [parent (first-parent parents)] [shallow? (shallow-repository? repo)] @@ -1228,7 +1253,10 @@ [line-count (added-line-count-for-paths repo rev paths file)] [lines (bounded-added-lines-for-paths repo rev paths file)] [python-lines (python-lines-for-commit repo rev paths file)] - [note-count (note-byte-count repo rev)] [note (bounded-note-text repo rev)] [note-obj (parse-note-object note)] + [raw-note (note-text-from-map note-map rev)] + [note-count (string-length raw-note)] + [note (bounded-string raw-note (scan-config-max-note-bytes current-config))] + [note-obj (parse-note-object note)] [attribution (note-attributions note-obj)] [metadata (metadata-hits author-name author-email subject body note)] [eligible? (and (pair? files) (pair? lines) (or (= min-lines 0) (>= (length lines) min-lines)))] @@ -1262,11 +1290,12 @@ (cadr sim-pair)))) (def (scan-repo repo revs file min-lines metadata-only? heuristics-only?) - (let loop ([xs revs] [hashes '()] [out '()]) - (if (null? xs) - (reverse out) - (let ([pair (scan-one repo (car xs) hashes revs file min-lines metadata-only? heuristics-only?)]) - (loop (cdr xs) (cons (cadr pair) hashes) (cons (car pair) out)))))) + (let ([note-map (selected-note-map repo revs)]) + (let loop ([xs revs] [hashes '()] [out '()]) + (if (null? xs) + (reverse out) + (let ([pair (scan-one repo (car xs) note-map hashes revs file min-lines metadata-only? heuristics-only?)]) + (loop (cdr xs) (cons (cadr pair) hashes) (cons (car pair) out))))))) (def (json-escape-to s port) (put-char port #\") --- a/tests/fixture-smoke.sh +++ b/tests/fixture-smoke.sh @@ -153,6 +153,9 @@ if printf '%s\n' "$git_ai_json" | grep -q 'refs/notes/ai note is not supported J echo "git-ai authorship note should not be reported as unsupported JSON" >&2 exit 1 fi +git_ai_head_json=$("$root/bin/jerboa-aigit" scan "$git_ai_note_fixture" --commit HEAD --format json) +printf '%s\n' "$git_ai_head_json" | grep -q '"recorded_ai_note_present":true' +printf '%s\n' "$git_ai_head_json" | grep -q '"format":"git-ai-authorship/3.0.0"' git -C "$identity_fixture" init -q git -C "$identity_fixture" config user.name "Human Decoder"