Add repository baseline signal
ober
8680c9243b732c9dfca727693ec7310fc75dce95
--- a/README.md +++ b/README.md @@ -80,6 +80,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. +Baseline signals are reported as contextual evidence with zero default weight. +The author baseline uses same-author addition history; the repository baseline +uses median/MAD-style addition-count deviation across the selected scan window. + Warnings also call out root commits with no parent baseline, merge commits where the scanner intentionally uses the first-parent diff, missing parent objects in shallow history, and binary file changes that are skipped by text-line --- a/main-binary.ss +++ b/main-binary.ss @@ -553,6 +553,30 @@ [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")))) +(def (median xs) + (if (null? xs) + 0.0 + (let* ([sorted (sort xs <)] + [n (length sorted)] + [mid (floor (/ n 2))]) + (if (= 0 (modulo n 2)) + (/ (+ (safe-ref sorted (- mid 1) 0) (safe-ref sorted mid 0)) 2.0) + (safe-ref sorted mid 0))))) + +(def (mad xs med) + (median (map (lambda (x) (abs (- x med))) xs))) + +(def (repository-baseline-signal additions repo-additions) + (if (< (length repo-additions) 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")))) (def (independent-hits signals) (length (unique (map signal-category (filter (lambda (s) (>= (signal-score s) 0.30)) signals))))) @@ -577,6 +601,14 @@ (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) out)))))) +(def (prior-repository-additions 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) out)))))) (def (author-times repo revs author-id) (let loop ([xs revs] [out '()]) @@ -641,7 +673,8 @@ [raw-signals (if eligible? (list (message-signal subject body adds) (code-signal lines) (structure-signal paths adds dels lines) (car sim-pair) (history-signal adds time (parent-time repo parent) (author-times repo revs author-email)) - (baseline-signal adds (prior-additions repo revs author-email rev file))) + (baseline-signal adds (prior-additions repo revs author-email rev file)) + (repository-baseline-signal adds (prior-repository-additions repo revs rev file))) '())] [signals (if metadata-only? '() (map (lambda (s) (with-config-weight current-config s)) raw-signals))] [score (if metadata-only? 0.0 (aggregate-score signals))] --- a/tests/fixture-smoke.sh +++ b/tests/fixture-smoke.sh @@ -57,6 +57,7 @@ printf '%s\n' "$json" | grep -q '"category":"structure"' printf '%s\n' "$json" | grep -q '"category":"similarity"' printf '%s\n' "$json" | grep -q '"category":"history"' printf '%s\n' "$json" | grep -q '"category":"baseline"' +printf '%s\n' "$json" | grep -q '"name":"repository-baseline"' printf '%s\n' "$json" | grep -q '"evidence":\[\]' printf '%s\n' "$json" | grep -q '"warnings":\[\]' if printf '%s\n' "$json" | grep -q 'vendor/library.py'; then