Expand author and repository baselines

ober

08f27c697ec209823171ae02c8364e2dd913829c

diff --git a/GAPS.md b/GAPS.md
index 110c7d9..7e8fa5e 100644
--- 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
diff --git a/main-binary.ss b/main-binary.ss
index b27d640..650cb39 100644
--- 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))]
diff --git a/tests/fixture-smoke.sh b/tests/fixture-smoke.sh
index dfd27e7..638b9d7 100755
--- 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