Add file attribution summaries
ober
ce6da463e46439e8be4660558b98c8c05d1f7e36
--- a/README.md +++ b/README.md @@ -108,6 +108,10 @@ JSON output includes: - recorded AI note presence and excerpt - structured `recorded_attribution` entries for supported Git AI note line ranges (`source`, `tool`, `model`, `session`, `path`, `start`, `end`) +- `file_findings` entries that attach matching recorded note ranges to changed + files without duplicating commit-level heuristic scores +- `unmatched_recorded_attribution` for recorded ranges whose path was not part + of the changed-file set - metadata hits - raw signal scores, weights, evidence, and limitations - aggregate score and verdict --- a/main-binary.ss +++ b/main-binary.ss @@ -694,6 +694,27 @@ (list (cons 'name (signal-name s)) (cons 'category (signal-category s)) (cons 'score (signal-score s)) (cons 'weight (signal-weight s)) (cons 'confidence (signal-confidence s)) (cons 'reason (signal-reason s)) (cons 'evidence (signal-evidence s)) (cons 'limitations (signal-limitations s)))) +(def (attribution-path item) + (alist-ref/default item 'path "")) + +(def (attribution-for-path attribution path) + (filter (lambda (item) (same-public-string? (attribution-path item) path)) attribution)) + +(def (path-listed? path paths) + (for/or ([p paths]) (same-public-string? p path))) + +(def (unmatched-attribution attribution paths) + (filter (lambda (item) (not (path-listed? (attribution-path item) paths))) attribution)) + +(def (file-finding-json f path) + (let ([items (attribution-for-path (finding-attribution f) path)]) + (list (cons 'path path) + (cons 'recorded_attribution items) + (cons 'recorded_ai_lines (sum (map attribution-line-count items))) + (cons 'commit_signal_categories (unique (map signal-category (finding-signals f)))) + (cons 'commit_score (finding-score f)) + (cons 'commit_verdict (finding-verdict f)) + (cons 'attribution_status (if (pair? items) "matched-note-range" "no-recorded-range"))))) (def (finding-json repo f) (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) @@ -705,6 +726,8 @@ (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 '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))) (cons 'score (finding-score f)) (cons 'verdict (finding-verdict f)) (cons 'warnings (finding-warnings f)))) (def (report-json repo findings) --- a/tests/fixture-smoke.sh +++ b/tests/fixture-smoke.sh @@ -48,6 +48,9 @@ printf '%s\n' "$json" | grep -q '"network_used":false' printf '%s\n' "$json" | grep -q '"verdict":"recorded-ai-authorship"' printf '%s\n' "$json" | grep -q '"recorded_ai_note_present":true' printf '%s\n' "$json" | grep -q '"recorded_attribution":\[{"source":"refs/notes/ai","tool":"codex","model":"gpt-5","session":"","path":"src/generated.py","start":1,"end":360}\]' +printf '%s\n' "$json" | grep -q '"file_findings":\[{"path":"src/generated.py","recorded_attribution":\[{"source":"refs/notes/ai","tool":"codex","model":"gpt-5","session":"","path":"src/generated.py","start":1,"end":360}\],"recorded_ai_lines":360' +printf '%s\n' "$json" | grep -q '"attribution_status":"matched-note-range"' +printf '%s\n' "$json" | grep -q '"unmatched_recorded_attribution":\[\]' printf '%s\n' "$json" | grep -q '"metadata_hits":\["codex","openai"\]' printf '%s\n' "$json" | grep -q '"category":"code"' printf '%s\n' "$json" | grep -q '"category":"structure"' @@ -174,6 +177,11 @@ printf '%s\n' "$threshold_json" | grep -q '"verdict":"likely-human-style"' threshold_triplet_json=$("$root/bin/jerboa-aigit" scan "$fixture" --format json --count 1 --heuristics-only --threshold 0.99,0.995,1.0) printf '%s\n' "$threshold_triplet_json" | grep -q '"verdict":"likely-human-style"' +git -C "$fixture" notes --ref=ai remove HEAD >/dev/null +git -C "$fixture" notes --ref=ai add -m '{"tool":"codex","model":"gpt-5","lines":[{"path":"missing.py","start":1,"end":2}]}' HEAD +unmatched_json=$("$root/bin/jerboa-aigit" scan "$fixture" --format json --count 1) +printf '%s\n' "$unmatched_json" | grep -q '"unmatched_recorded_attribution":\[{"source":"refs/notes/ai","tool":"codex","model":"gpt-5","session":"","path":"missing.py","start":1,"end":2}\]' + printf 'not json\n' > "$config_file" bad_config_table=$("$root/bin/jerboa-aigit" scan "$fixture" --config "$config_file" --count 1) printf '%s\n' "$bad_config_table" | grep -q 'commit score verdict'