Add Cadence burst timing parity
ober
7614df9efde4da60ac8018f77dfc8645386c6ceb
--- a/GAPS.md +++ b/GAPS.md @@ -210,6 +210,11 @@ Acceptance criteria: Current timing handles velocity and simple nearby commit count, but not full Cadence-style rapid-fire burst definitions. +Status: implemented with explicit high-velocity evidence, burst windows that +require at least five nearby commits and at least three substantial nonblank +commits, zero/negative parent-time gap evidence for skewed or historical +imports, and fixture coverage for burst, normal batch, and skewed-date cases. + Acceptance criteria: - Implement burst windows requiring at least five commits in ten minutes and at --- a/main-binary.ss +++ b/main-binary.ss @@ -345,6 +345,9 @@ (def (added-line-count-for-paths repo rev paths explicit-file) (length (added-lines-for-paths repo rev paths explicit-file))) +(def (commit-nonblank-added-line-count repo rev file) + (count-where (lambda (line) (not (blank? line))) + (bounded-added-lines-for-paths repo rev (numstat-paths (changed-files repo rev file)) file))) (def known-agents '("codex" "copilot" "claude" "cursor" "openai" "anthropic" "aider" "windsurf" "cody" "tabnine" "ai-agent")) @@ -736,15 +739,19 @@ "similarity in one scan window cannot prove original authorship") hash))) -(def (history-signal additions commit-time parent-time author-times) +(def (history-signal additions commit-time parent-time author-events) (let* ([delta (- commit-time parent-time)] + [non-positive-gap? (and (> parent-time 0) (<= delta 0))] [minutes (if (> delta 0) (/ delta 60.0) 0.0)] [velocity (if (> minutes 0.0) (/ additions minutes) 0.0)] - [nearby (count-where (lambda (t) (<= (abs (- commit-time t)) 600)) author-times)] - [burst? (>= nearby 5)] + [nearby-events (filter (lambda (event) (<= (abs (- commit-time (car event))) 600)) author-events)] + [nearby (length nearby-events)] + [substantial (count-where (lambda (event) (>= (cdr event) 20)) nearby-events)] + [burst? (and (>= nearby 5) (>= substantial 3))] [score (+ (cond [(> velocity 50.0) 1.0] [(> velocity 20.0) 0.60] [else 0.0]) (if burst? 0.20 0.0))] [evidence (append (if (> velocity 20.0) (list (str "line velocity " velocity " additions/minute")) '()) - (if burst? (list (str nearby " commits by author inside ten minutes")) '()))]) + (if burst? (list (str nearby " commits by author inside ten minutes; " substantial " substantial")) '()) + (if non-positive-gap? (list (str "non-positive parent time gap " delta " seconds")) '()))]) (sig "history-timing" "history" score 0.15 "low" "commit timestamps suggest high throughput or bursts" evidence "Git commit timestamps are not typing timestamps"))) @@ -816,14 +823,17 @@ (loop (cdr xs) (cons (numstat-adds (changed-files repo rev file)) out)) (loop (cdr xs) out)))))) -(def (author-times repo revs author-id) +(def (author-events repo revs author-id file) (let loop ([xs revs] [out '()]) (if (null? xs) out - (let* ([fields (commit-fields repo (car xs))] + (let* ([rev (car xs)] + [fields (commit-fields repo rev)] [e (safe-ref fields 3 "")] [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)))))) + (if (same-public-string? e author-id) + (loop (cdr xs) (cons (cons t (commit-nonblank-added-line-count repo rev file)) out)) + (loop (cdr xs) out)))))) (def (commit-shape-warnings parents) (append (if (root-commit? parents) '("root commit; no parent diff baseline") '()) @@ -887,7 +897,7 @@ (structure-signal paths adds dels lines) (cadence-diff-shape-signal files paths adds dels) (semantic-alignment-signal subject body lines) - (car sim-pair) (history-signal adds time (parent-time repo parent) (author-times repo revs author-email)) + (car sim-pair) (history-signal adds time (parent-time repo parent) (author-events repo revs author-email file)) (baseline-signal adds (prior-additions repo revs author-email rev file)) (repository-baseline-signal adds (prior-repository-additions repo revs rev file))) '())] --- a/tests/fixture-smoke.sh +++ b/tests/fixture-smoke.sh @@ -6,7 +6,9 @@ fixture=$(mktemp -d) shallow=$(mktemp -d) provider_tmp=$(mktemp -d) shape_fixture=$(mktemp -d) -trap 'rm -rf "$fixture" "$shallow" "$provider_tmp" "$shape_fixture"' EXIT +timing_fixture=$(mktemp -d) +normal_timing_fixture=$(mktemp -d) +trap 'rm -rf "$fixture" "$shallow" "$provider_tmp" "$shape_fixture" "$timing_fixture" "$normal_timing_fixture"' EXIT git -C "$fixture" init -q git -C "$fixture" config user.name "Human Dev" @@ -100,6 +102,80 @@ printf '%s\n' "$shape_json" | grep -q '"name":"cadence-diff-shape"' printf '%s\n' "$shape_json" | grep -q 'skewed addition/deletion ratio' printf '%s\n' "$shape_json" | grep -q 'template/generated path count' +git -C "$timing_fixture" init -q +git -C "$timing_fixture" config user.name "Burst Bot" +git -C "$timing_fixture" config user.email "burst@example.test" +printf 'base\n' > "$timing_fixture/base.txt" +git -C "$timing_fixture" add base.txt +GIT_AUTHOR_DATE='2026-07-29T07:00:00-06:00' \ +GIT_COMMITTER_DATE='2026-07-29T07:00:00-06:00' \ + git -C "$timing_fixture" commit -q -m 'base timing fixture' +i=1 +while [ "$i" -le 5 ]; do + file="$timing_fixture/burst-$i.py" + j=1 + while [ "$j" -le 25 ]; do + printf 'def helper_%s_%s(value): return value + %s\n' "$i" "$j" "$j" >> "$file" + j=$((j + 1)) + done + git -C "$timing_fixture" add "burst-$i.py" + minute=$(printf '%02d' "$i") + GIT_AUTHOR_DATE="2026-07-29T07:$minute:00-06:00" \ + GIT_COMMITTER_DATE="2026-07-29T07:$minute:00-06:00" \ + git -C "$timing_fixture" commit -q -m "burst commit $i" + i=$((i + 1)) +done +burst_json=$("$root/bin/jerboa-aigit" scan "$timing_fixture" --format json --count 5) +printf '%s\n' "$burst_json" | grep -q 'line velocity' +printf '%s\n' "$burst_json" | grep -q 'commits by author inside ten minutes; 5 substantial' + +printf 'clock skew\n' >> "$timing_fixture/base.txt" +git -C "$timing_fixture" add base.txt +GIT_AUTHOR_DATE='2026-07-29T06:59:00-06:00' \ +GIT_COMMITTER_DATE='2026-07-29T06:59:00-06:00' \ + git -C "$timing_fixture" commit -q -m 'clock skew commit' +skew_json=$("$root/bin/jerboa-aigit" scan "$timing_fixture" --format json --count 1) +printf '%s\n' "$skew_json" | grep -q 'non-positive parent time gap' + +git -C "$normal_timing_fixture" init -q +git -C "$normal_timing_fixture" config user.name "Batch Bot" +git -C "$normal_timing_fixture" config user.email "batch@example.test" +printf 'base\n' > "$normal_timing_fixture/base.txt" +git -C "$normal_timing_fixture" add base.txt +GIT_AUTHOR_DATE='2026-07-29T10:00:00-06:00' \ +GIT_COMMITTER_DATE='2026-07-29T10:00:00-06:00' \ + git -C "$normal_timing_fixture" commit -q -m 'base normal timing fixture' +i=1 +while [ "$i" -le 5 ]; do + file="$normal_timing_fixture/batch-$i.py" + j=1 + while [ "$j" -le 25 ]; do + printf 'def batch_%s_%s(value): return value + %s\n' "$i" "$j" "$j" >> "$file" + j=$((j + 1)) + done + git -C "$normal_timing_fixture" add "batch-$i.py" + case "$i" in + 1) stamp='2026-07-29T10:15:00-06:00' ;; + 2) stamp='2026-07-29T10:30:00-06:00' ;; + 3) stamp='2026-07-29T10:45:00-06:00' ;; + 4) stamp='2026-07-29T11:00:00-06:00' ;; + *) stamp='2026-07-29T11:15:00-06:00' ;; + esac + GIT_AUTHOR_DATE="$stamp" \ + GIT_COMMITTER_DATE="$stamp" \ + git -C "$normal_timing_fixture" commit -q -m "normal batch commit $i" + i=$((i + 1)) +done +normal_json=$("$root/bin/jerboa-aigit" scan "$normal_timing_fixture" --format json --count 5) +if printf '%s\n' "$normal_json" | grep -q 'commits by author inside ten minutes'; then + echo "normal spaced batch should not trigger timing burst evidence" >&2 + exit 1 +fi +if printf '%s\n' "$normal_json" | grep -q 'line velocity'; then + echo "normal spaced batch should not trigger high-velocity evidence" >&2 + exit 1 +fi + heuristics=$("$root/bin/jerboa-aigit" scan "$fixture" --format json --count 1 --heuristics-only) printf '%s\n' "$heuristics" | grep -q '"recorded_ai_note_present":true' if printf '%s\n' "$heuristics" | grep -q '"verdict":"recorded-ai-authorship"'; then