Add JSON stats output

ober

5b2dd4dbf2adf89848d3b025f1a094ebb9f7a27a

diff --git a/README.md b/README.md
index 187da7e..df164f3 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,8 @@ and `network_used:false`.
 
 `stats` reports commit count, distinct authors, recorded tools, recorded AI line
 counts, metadata/heuristic counts, score-band counts, and signal-category
-coverage for the selected revision range.
+coverage for the selected revision range. Use `stats --format json` for the
+same summary as a stable machine-readable object.
 
 `explain` prints the selected commit's thresholds, raw signals, weighted signal
 contributions, corroboration bonus, counter-evidence from zero-score signals,
diff --git a/main-binary.ss b/main-binary.ss
index ca5f020..7b8fe13 100644
--- a/main-binary.ss
+++ b/main-binary.ss
@@ -883,6 +883,38 @@
 (def (finding-has-signal-category? f category)
   (for/or ([s (finding-signals f)])
     (same-public-string? (signal-category s) category)))
+(def (count-verdict findings verdict-name)
+  (count-where (lambda (f) (same-public-string? (finding-verdict f) verdict-name)) findings))
+
+(def (score-band-json findings verdict-name)
+  (list (cons 'name verdict-name)
+        (cons 'count (count-verdict findings verdict-name))))
+
+(def (signal-coverage-json findings category)
+  (list (cons 'category category)
+        (cons 'count (count-where (lambda (f) (finding-has-signal-category? f category)) findings))))
+
+(def (stats-json repo findings)
+  (let ([tools (stats-tools findings)])
+    (list (cons 'detector_version detector-version)
+          (cons 'config_hash (scan-config-hash current-config))
+          (cons 'analysis_provider current-analysis-provider)
+          (cons 'provider current-requested-provider)
+          (cons 'llm_used current-llm-used?)
+          (cons 'network_used current-network-used?)
+          (cons 'repository repo)
+          (cons 'count (length findings))
+          (cons 'author_count (length (unique (map finding-author-email findings))))
+          (cons 'tools tools)
+          (cons 'recorded_ai_notes (count-where (lambda (f) (not (string-empty? (finding-note f)))) findings))
+          (cons 'recorded_ai_lines (sum (map finding-recorded-line-count findings)))
+          (cons 'metadata_indicated_agents (count-where (lambda (f) (pair? (finding-metadata f))) findings))
+          (cons 'likely_ai_assisted_by_heuristics (count-verdict findings "likely-ai-assisted"))
+          (cons 'score_bands (map (lambda (v) (score-band-json findings v)) stats-verdicts))
+          (cons 'signal_coverage (map (lambda (category) (signal-coverage-json findings category)) stats-signal-categories)))))
+
+(def (display-stats-json repo findings)
+  (displayln (json-string (stats-json repo findings))))
 (def (display-stats findings)
   (let ([tools (stats-tools findings)])
     (displayln (str "commits: " (length findings)))
@@ -1135,7 +1167,10 @@
                  [revs (commit-list root (options-count opts) commit (options-from opts) (options-to opts) (options-file opts))]
                  [findings (scan-repo root revs (options-file opts) (options-min-lines opts)
                                       (options-metadata-only? opts) (options-heuristics-only? opts))])
-            (cond [(string=? (options-command opts) "stats") (display-stats findings)]
+            (cond [(string=? (options-command opts) "stats")
+                   (if (string=? (options-format opts) "json")
+                       (display-stats-json root findings)
+                       (display-stats findings))]
                   [(string=? (options-command opts) "verify-authorship") (display-json root findings)]
                   [(string=? (options-command opts) "explain")
                    (if (pair? findings)
diff --git a/tests/fixture-smoke.sh b/tests/fixture-smoke.sh
index d64f886..27103cf 100755
--- a/tests/fixture-smoke.sh
+++ b/tests/fixture-smoke.sh
@@ -99,6 +99,14 @@ printf '%s\n' "$stats" | grep -q '  recorded-ai-authorship: 1'
 printf '%s\n' "$stats" | grep -q 'signal coverage:'
 printf '%s\n' "$stats" | grep -q '  code: 2'
 
+stats_json=$("$root/bin/jerboa-aigit" stats "$fixture" --count 2 --format json)
+printf '%s\n' "$stats_json" | grep -q '"count":2'
+printf '%s\n' "$stats_json" | grep -q '"author_count":2'
+printf '%s\n' "$stats_json" | grep -q '"tools":\["codex"\]'
+printf '%s\n' "$stats_json" | grep -q '"recorded_ai_lines":360'
+printf '%s\n' "$stats_json" | grep -q '"score_bands":\[{"name":"recorded-ai-authorship","count":1}'
+printf '%s\n' "$stats_json" | grep -q '"signal_coverage":\[{"category":"text","count":2}'
+
 jsonl=$("$root/bin/jerboa-aigit" scan "$fixture" --format jsonl --count 2)
 jsonl_lines=$(printf '%s\n' "$jsonl" | wc -l | tr -d ' ')
 test "$jsonl_lines" = 2