Expand author and repository baselines
ober
08f27c697ec209823171ae02c8364e2dd913829c
--- a/GAPS.md +++ b/GAPS.md @@ -176,6 +176,12 @@ Acceptance criteria: variance, function length, and naming style. This project baselines addition counts only. +Status: implemented for author and repository baselines using additions, +comment ratio, average line length, line-length variance, generic-token ratio, +and a blank-line block-size proxy. Baselines remain zero-weight contextual +signals so insufficient or anomalous history does not directly change the final +score. + Acceptance criteria: - Extend author and repository baselines to include comment ratio, average line --- a/main-binary.ss +++ b/main-binary.ss @@ -606,6 +606,53 @@ [(blank? (car xs)) (if last (loop (cdr xs) (+ idx 1) idx (cons (- idx last) out)) (loop (cdr xs) (+ idx 1) idx out))] [else (loop (cdr xs) (+ idx 1) last out)]))) +(def (generic-token-ratio lines) + (let* ([lexemes (source-lexemes lines)] + [generic-count (count-where (lambda (lexeme) (member lexeme generic-names)) lexemes)]) + (if (null? lexemes) 0.0 (/ (exact->inexact generic-count) (length lexemes))))) +(def (comment-line-ratio lines) + (let* ([nonblank (filter (lambda (line) (not (blank? line))) lines)] + [comments (count-where comment-line? nonblank)]) + (if (null? nonblank) 0.0 (/ (exact->inexact comments) (length nonblank))))) +(def (block-size-proxy lines) + (let ([gaps (blank-gaps lines)]) + (if (null? gaps) + (exact->inexact (length (filter (lambda (line) (not (blank? line))) lines))) + (mean gaps)))) +(def baseline-feature-names + '("additions" "comment ratio" "avg line length" "line variance" "generic ratio" "block proxy")) +(def (baseline-features additions lines) + (let* ([lengths (line-lengths lines)] + [line-sd (stddev lengths)]) + (list (exact->inexact additions) + (comment-line-ratio lines) + (mean lengths) + (* line-sd line-sd) + (generic-token-ratio lines) + (block-size-proxy lines)))) +(def (transpose rows) + (if (or (null? rows) (null? (car rows))) + '() + (cons (map car rows) (transpose (map cdr rows))))) +(def (baseline-feature-zs current history) + (let loop ([names baseline-feature-names] [values current] [columns (transpose history)] [out '()]) + (if (or (null? names) (null? values) (null? columns)) + (reverse out) + (let* ([column (car columns)] + [sd (stddev column)] + [z (if (= sd 0.0) 0.0 (/ (- (car values) (mean column)) sd))]) + (loop (cdr names) (cdr values) (cdr columns) (cons (list (car names) z) out)))))) +(def (baseline-z-score item) (cadr item)) +(def (baseline-evidence prefix zs threshold) + (map (lambda (item) (str prefix " " (car item) " z-score " (baseline-z-score item))) + (filter (lambda (item) (> (abs (baseline-z-score item)) threshold)) zs))) +(def (baseline-score zs) + (let ([hits (count-where (lambda (item) (> (abs (baseline-z-score item)) 2.0)) zs)] + [strong (count-where (lambda (item) (> (abs (baseline-z-score item)) 3.0)) zs)]) + (cond [(>= strong 2) 0.50] + [(>= hits 3) 0.35] + [(>= hits 1) 0.20] + [else 0.0]))) (def (structure-signal paths additions deletions lines) (let* ([lengths (line-lengths lines)] @@ -945,17 +992,15 @@ (sig "history-timing" "history" score 0.15 "low" "commit timestamps suggest high throughput or bursts" evidence "Git commit timestamps are not typing timestamps"))) -(def (baseline-signal additions author-additions) - (if (< (length author-additions) 5) +(def (baseline-signal current-features author-features) + (if (< (length author-features) 5) (sig "author-baseline" "baseline" 0.0 0.00 "low" "author baseline unavailable" '("fewer than five comparison commits") "missing baseline is not evidence of human authorship") - (let* ([m (mean author-additions)] - [sd (stddev author-additions)] - [z (if (= sd 0.0) 0.0 (/ (- additions m) sd))] - [score (cond [(> z 3.0) 0.50] [(> z 2.0) 0.25] [else 0.0])] - [evidence (if (> score 0.0) (list (str "addition-count z-score " z)) '())]) - (sig "author-baseline" "baseline" score 0.00 "low" "commit size differs from author baseline" evidence - "baseline is contextual in this version and is not directly weighted")))) + (let* ([zs (baseline-feature-zs current-features author-features)] + [score (baseline-score zs)] + [evidence (baseline-evidence "author" zs 2.0)]) + (sig "author-baseline" "baseline" score 0.00 "low" "commit style differs from author baseline" evidence + "baseline uses commit-size and lexical/style features as contextual zero-weight evidence")))) (def (median xs) (if (null? xs) 0.0 @@ -969,17 +1014,15 @@ (def (mad xs med) (median (map (lambda (x) (abs (- x med))) xs))) -(def (repository-baseline-signal additions repo-additions) - (if (< (length repo-additions) 5) +(def (repository-baseline-signal current-features repo-features) + (if (< (length repo-features) 5) (sig "repository-baseline" "baseline" 0.0 0.00 "low" "repository baseline unavailable" '("fewer than five comparison commits") "missing repository baseline is not evidence of human authorship") - (let* ([med (median repo-additions)] - [spread (mad repo-additions med)] - [robust-z (if (= spread 0.0) 0.0 (/ (abs (- additions med)) (* 1.4826 spread)))] - [score (cond [(> robust-z 6.0) 0.50] [(> robust-z 3.5) 0.25] [else 0.0])] - [evidence (if (> score 0.0) (list (str "repository addition robust-z " robust-z)) '())]) - (sig "repository-baseline" "baseline" score 0.00 "low" "commit size differs from repository baseline" evidence - "repository baseline uses commit-size history only and is not directly weighted")))) + (let* ([zs (baseline-feature-zs current-features repo-features)] + [score (baseline-score zs)] + [evidence (baseline-evidence "repository" zs 2.0)]) + (sig "repository-baseline" "baseline" score 0.00 "low" "commit style differs from repository baseline" evidence + "repository baseline uses commit-size and lexical/style features as contextual zero-weight evidence")))) (def (independent-hits signals) (length (unique (map signal-category (filter (lambda (s) (>= (signal-score s) 0.30)) signals))))) @@ -996,21 +1039,27 @@ [(>= score (scan-config-human-threshold current-config)) "mixed-uncertain"] [else "likely-human-style"])) -(def (prior-additions repo revs author-id current file) +(def (commit-baseline-features repo rev file) + (let* ([files (changed-files repo rev file)] + [paths (numstat-paths files)] + [adds (numstat-adds files)] + [lines (bounded-added-lines-for-paths repo rev paths file)]) + (baseline-features adds lines))) +(def (prior-baseline-features repo revs author-id current file) (let loop ([xs revs] [out '()]) (if (null? xs) out (let* ([rev (car xs)] [fields (commit-fields repo rev)] [e (safe-ref fields 3 "")]) (if (and (same-public-string? e author-id) (not (same-public-string? rev current))) - (loop (cdr xs) (cons (numstat-adds (changed-files repo rev file)) out)) + (loop (cdr xs) (cons (commit-baseline-features repo rev file) out)) (loop (cdr xs) out)))))) -(def (prior-repository-additions repo revs current file) +(def (prior-repository-baseline-features repo revs current file) (let loop ([xs revs] [out '()]) (if (null? xs) out (let ([rev (car xs)]) (if (not (same-public-string? rev current)) - (loop (cdr xs) (cons (numstat-adds (changed-files repo rev file)) out)) + (loop (cdr xs) (cons (commit-baseline-features repo rev file) out)) (loop (cdr xs) out)))))) (def (author-events repo revs author-id file) @@ -1082,6 +1131,7 @@ [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)))] +[current-baseline-features (baseline-features adds lines)] [sim-pair (similarity-signal lines hashes author-email)] [raw-signals (if eligible? (append @@ -1090,8 +1140,8 @@ (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-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))) + (baseline-signal current-baseline-features (prior-baseline-features repo revs author-email rev file)) + (repository-baseline-signal current-baseline-features (prior-repository-baseline-features repo revs rev file))) (python-signals python-lines)) '())] [weighted-signals (if metadata-only? '() (map (lambda (s) (with-config-weight current-config s)) raw-signals))] --- a/tests/fixture-smoke.sh +++ b/tests/fixture-smoke.sh @@ -9,7 +9,8 @@ shape_fixture=$(mktemp -d) timing_fixture=$(mktemp -d) normal_timing_fixture=$(mktemp -d) similarity_fixture=$(mktemp -d) -trap 'rm -rf "$fixture" "$shallow" "$provider_tmp" "$shape_fixture" "$timing_fixture" "$normal_timing_fixture" "$similarity_fixture"' EXIT +baseline_fixture=$(mktemp -d) +trap 'rm -rf "$fixture" "$shallow" "$provider_tmp" "$shape_fixture" "$timing_fixture" "$normal_timing_fixture" "$similarity_fixture" "$baseline_fixture"' EXIT git -C "$fixture" init -q git -C "$fixture" config user.name "Human Dev" @@ -236,6 +237,47 @@ similarity_json_two=$("$root/bin/jerboa-aigit" scan "$similarity_fixture" --form printf '%s\n' "$similarity_json_one" | grep -q 'best same-author simhash64 similarity' printf '%s\n' "$similarity_json_one" | grep -q 'best cross-author simhash64 similarity' +git -C "$baseline_fixture" init -q +git -C "$baseline_fixture" config user.name "Baseline Dev" +git -C "$baseline_fixture" config user.email "baseline@example.test" +i=1 +while [ "$i" -le 5 ]; do + file="$baseline_fixture/prior-$i.py" + j=1 + while [ "$j" -le $((i + 3)) ]; do + printf 'def domain_%s_%s(count): return count + %s\n' "$i" "$j" "$j" >> "$file" + j=$((j + 1)) + done + if [ "$i" -eq 3 ]; then + printf '# occasional human note\n' >> "$file" + printf 'result = count\n' >> "$file" + fi + git -C "$baseline_fixture" add "prior-$i.py" + minute=$(printf '%02d' "$i") + GIT_AUTHOR_DATE="2026-07-29T13:$minute:00-06:00" \ + GIT_COMMITTER_DATE="2026-07-29T13:$minute:00-06:00" \ + git -C "$baseline_fixture" commit -q -m "prior baseline commit $i" + i=$((i + 1)) +done +i=1 +while [ "$i" -le 60 ]; do + printf '# helper processor manager value result item data values helper processor manager value result item data values\n' >> "$baseline_fixture/outlier.py" + i=$((i + 1)) +done +git -C "$baseline_fixture" add outlier.py +GIT_AUTHOR_DATE='2026-07-29T13:10:00-06:00' \ +GIT_COMMITTER_DATE='2026-07-29T13:10:00-06:00' \ + git -C "$baseline_fixture" commit -q -m 'baseline style outlier' +baseline_json=$("$root/bin/jerboa-aigit" scan "$baseline_fixture" --format json --count 6) +printf '%s\n' "$baseline_json" | grep -q '"name":"author-baseline"' +printf '%s\n' "$baseline_json" | grep -q '"name":"repository-baseline"' +printf '%s\n' "$baseline_json" | grep -q 'author additions z-score' +printf '%s\n' "$baseline_json" | grep -q 'author avg line length z-score' +printf '%s\n' "$baseline_json" | grep -q 'author generic ratio z-score' +printf '%s\n' "$baseline_json" | grep -q 'repository additions z-score' +printf '%s\n' "$baseline_json" | grep -q 'repository block proxy z-score' +printf '%s\n' "$baseline_json" | grep -q 'baseline uses commit-size and lexical/style features as contextual zero-weight evidence' + 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