Add recovered attribution evidence

ober

781536d553917ef2b477fb3f40ea3ccefb2d2c70

diff --git a/GAPS.md b/GAPS.md
index 9f04609..dfc0b8a 100644
--- a/GAPS.md
+++ b/GAPS.md
@@ -331,6 +331,12 @@ Acceptance criteria:
 - Support known agent identity catalog from config.
 - Never label recovered attribution as recorded provenance.
 
+Status: implemented with `recovered_attribution` entries derived from known
+agent identity matches in author identity, commit messages, and refs/notes/ai
+metadata. Each item carries source, agent, confidence, evidence, and
+`recorded_provenance:false`; fixtures assert configured identities recover
+without populating `recorded_attribution`.
+
 ### G-033: Provider/tool catalog is hard-coded
 
 Known AI tools are detected through hard-coded strings.
diff --git a/main-binary.ss b/main-binary.ss
index b0814ea..e086576 100644
--- a/main-binary.ss
+++ b/main-binary.ss
@@ -27,7 +27,7 @@
 (defstruct signal (name category score weight confidence reason evidence limitations))
 (defstruct finding
   (commit parent author-name author-email time subject files additions deletions
-   added-lines note attribution metadata signals score verdict warnings))
+   added-lines note attribution recovered-attribution metadata signals score verdict warnings))
 
 (def raw-command-line (command-line))
 (def cli-args
@@ -480,6 +480,35 @@
   (let* ([text (down (string-join (list author-name author-email subject body note) "\n"))]
          [tokens (source-lexemes (list text))])
     (filter (lambda (marker) (identity-token-hit? tokens marker)) current-known-agents)))
+(def (identity-hits-in text)
+  (let ([tokens (source-lexemes (list (down text)))])
+    (filter (lambda (marker) (identity-token-hit? tokens marker)) current-known-agents)))
+
+(def (recovered-attribution-entry source agent confidence evidence)
+  (list (cons 'source source)
+        (cons 'agent agent)
+        (cons 'confidence confidence)
+        (cons 'recorded_provenance #f)
+        (cons 'evidence evidence)))
+
+(def (recovered-attributions-for-source source text confidence evidence)
+  (map (lambda (agent)
+         (recovered-attribution-entry source agent confidence evidence))
+       (identity-hits-in text)))
+
+(def (recovered-attributions author-name author-email subject body note)
+  (append (recovered-attributions-for-source "author_identity"
+                                             (string-join (list author-name author-email) "\n")
+                                             0.82
+                                             "known agent identity matched author name or email")
+          (recovered-attributions-for-source "commit_message"
+                                             (string-join (list subject body) "\n")
+                                             0.58
+                                             "known agent identity matched commit subject or body")
+          (recovered-attributions-for-source "recorded_note_metadata"
+                                             note
+                                             0.66
+                                             "known agent identity matched refs/notes/ai metadata")))
 
 (def (sig name category score weight confidence reason evidence limitations)
   (make-signal name category (clamp01 score) weight confidence reason evidence limitations))
@@ -1258,6 +1287,7 @@
          [note (bounded-string raw-note (scan-config-max-note-bytes current-config))]
          [note-obj (parse-note-object note)]
          [attribution (note-attributions note-obj)]
+         [recovered (recovered-attributions author-name author-email subject body note)]
          [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)]
@@ -1283,7 +1313,7 @@
          [v (cond [insufficient? "insufficient-evidence"]
                   [heuristics-only? (verdict score '() "")]
                   [else (verdict score metadata note)])])
-    (list (make-finding id parent author-name author-email time subject paths adds dels (length lines) note attribution metadata signals score v
+    (list (make-finding id parent author-name author-email time subject paths adds dels (length lines) note attribution recovered metadata signals score v
                         (append (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count parents shallow? binary-count rename-copy-count missing-parent?)
                                 (cadr embedding-pair)
                                 (cadr provider-pair)))
@@ -1363,6 +1393,7 @@
         (cons 'recorded_ai_note_present (not (string-empty? (finding-note f))))
         (cons 'recorded_ai_note_excerpt (if (string-empty? (finding-note f)) "" (substring (finding-note f) 0 (min 400 (string-length (finding-note f))))))
         (cons 'recorded_attribution (finding-attribution f))
+        (cons 'recovered_attribution (finding-recovered-attribution f))
         (cons 'file_findings (map (lambda (path) (file-finding-json f path)) (finding-files f)))
         (cons 'unmatched_recorded_attribution (unmatched-attribution (finding-attribution f) (finding-files f)))
         (cons 'metadata_hits (finding-metadata f)) (cons 'signals (map signal-json (finding-signals f)))
@@ -1402,12 +1433,22 @@
 
 (def (zero-score-signals signals)
   (filter (lambda (s) (= (signal-score s) 0.0)) signals))
+(def (display-recovered-attributions items)
+  (if (pair? items)
+      (begin
+        (displayln "recovered attribution:")
+        (for ([item items])
+          (displayln (str "- agent=" (alist-ref/default item 'agent "")
+                          " source=" (alist-ref/default item 'source "")
+                          " confidence=" (alist-ref/default item 'confidence 0.0)
+                          " recorded_provenance=false"))))))
 (def (display-explain f)
   (displayln (str "commit: " (finding-commit f)))
   (displayln (str "verdict: " (finding-verdict f)))
   (displayln (str "score: " (finding-score f)))
   (displayln (str "author: " (finding-author-name f) " <" (finding-author-email f) ">"))
   (displayln (str "subject: " (finding-subject f)))
+  (display-recovered-attributions (finding-recovered-attribution f))
   (displayln (str "thresholds: human=" (scan-config-human-threshold current-config)
                   " ai=" (scan-config-ai-threshold current-config)
                   " result=" (threshold-description (finding-score f))))
diff --git a/tests/fixture-smoke.sh b/tests/fixture-smoke.sh
index bfcd2df..4635e60 100755
--- a/tests/fixture-smoke.sh
+++ b/tests/fixture-smoke.sh
@@ -178,6 +178,8 @@ identity_config="$identity_fixture/aigit.json"
 printf '{"known_agents":["mybot"]}\n' > "$identity_config"
 identity_custom_json=$("$root/bin/jerboa-aigit" scan "$identity_fixture" --config "$identity_config" --format json --count 1)
 printf '%s\n' "$identity_custom_json" | grep -q '"metadata_hits":\["mybot"\]'
+printf '%s\n' "$identity_custom_json" | grep -q '"recorded_attribution":\[\]'
+printf '%s\n' "$identity_custom_json" | grep -q '"recovered_attribution":\[{"source":"author_identity","agent":"mybot","confidence":0.82,"recorded_provenance":false'
 
 git -C "$shape_fixture" init -q
 git -C "$shape_fixture" config user.name "Human Dev"