Warn on shallow history

ober

db731d12bf838eff3cec0efd992190df3d3c61be

diff --git a/README.md b/README.md
index ee0de67..e5656c3 100644
--- a/README.md
+++ b/README.md
@@ -62,8 +62,9 @@ analyzed per commit, and 50,000 bytes per AI note. When a limit is hit, output
 keeps the bounded data and includes a warning.
 
 Warnings also call out root commits with no parent baseline, merge commits where
-the scanner intentionally uses the first-parent diff, and binary file changes
-that are skipped by text-line heuristics.
+the scanner intentionally uses the first-parent diff, missing parent objects in
+shallow history, and binary file changes that are skipped by text-line
+heuristics.
 
 ## What It Reads
 
diff --git a/main-binary.ss b/main-binary.ss
index 43d6bcb..23e06f6 100644
--- a/main-binary.ss
+++ b/main-binary.ss
@@ -142,10 +142,22 @@
   (try
    (run-process (append (list "git" "-C" repo "--literal-pathspecs") args))
    (catch (e) "")))
+(def (git-ok? repo args)
+  (try
+   (begin
+     (run-process (append (list "git" "-C" repo "--literal-pathspecs") args))
+     #t)
+   (catch (e) #f)))
+
+(def (commit-available? repo rev)
+  (or (string-empty? rev)
+      (git-ok? repo (list "cat-file" "-e" (str rev "^{commit}")))))
 
 (def (repo-root path)
   (let ([root (string-trim (git path '("rev-parse" "--show-toplevel")))])
     (if (string-empty? root) #f root)))
+(def (shallow-repository? repo)
+  (same-public-string? (string-trim (git repo '("rev-parse" "--is-shallow-repository"))) "true"))
 
 (def (commit-list repo count commit from to file)
   (if commit
@@ -525,14 +537,18 @@
 (def (commit-shape-warnings parents)
   (append (if (root-commit? parents) '("root commit; no parent diff baseline") '())
           (if (merge-commit? parents) '("merge commit; scanner uses first-parent diff") '())))
+(def (parent-history-warnings shallow? missing-parent?)
+  (append (if shallow? '("repository is shallow; parent history may be unavailable") '())
+          (if missing-parent? '("parent object unavailable; history may be shallow") '())))
 
 (def (binary-change-warnings binary-count)
   (if (> binary-count 0)
       (list (str "binary file changes skipped: " binary-count))
       '()))
 
-(def (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count parents binary-count)
+(def (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count parents shallow? binary-count missing-parent?)
   (append (commit-shape-warnings parents)
+          (parent-history-warnings shallow? missing-parent?)
           (binary-change-warnings binary-count)
           (if (null? files) '("no changed text files found or commit is unavailable") '())
           (if (null? lines) '("no added UTF-8 patch lines available") '())
@@ -549,6 +565,8 @@
 (def (scan-one repo rev hashes revs file min-lines metadata-only? heuristics-only?)
   (let* ([fields (commit-fields repo rev)]
          [id (safe-ref fields 0 rev)] [parents (safe-ref fields 1 "")] [parent (first-parent parents)]
+         [shallow? (shallow-repository? repo)]
+         [missing-parent? (and (not (string-empty? parent)) (not (commit-available? repo parent)))]
          [author-name (safe-ref fields 2 "")] [author-email (safe-ref fields 3 "")]
          [time (parse-int (safe-ref fields 4 "0") 0)] [subject (safe-ref fields 5 "")]
          [body (commit-message repo rev)] [file-count (changed-file-count repo rev file)]
@@ -574,7 +592,7 @@
                   [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
-                        (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count parents binary-count))
+                        (warnings files lines note note-obj metadata-only? heuristics-only? min-lines file-count line-count note-count parents shallow? binary-count missing-parent?))
           (cadr sim-pair))))
 
 (def (scan-repo repo revs file min-lines metadata-only? heuristics-only?)
diff --git a/tests/fixture-smoke.sh b/tests/fixture-smoke.sh
index b219b79..5ea2a2e 100755
--- a/tests/fixture-smoke.sh
+++ b/tests/fixture-smoke.sh
@@ -3,7 +3,8 @@ set -eu
 
 root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
 fixture=$(mktemp -d)
-trap 'rm -rf "$fixture"' EXIT
+shallow=$(mktemp -d)
+trap 'rm -rf "$fixture" "$shallow"' EXIT
 
 git -C "$fixture" init -q
 git -C "$fixture" config user.name "Human Dev"
@@ -203,6 +204,11 @@ binary_json=$("$root/bin/jerboa-aigit" scan "$fixture" --format json --commit "$
 printf '%s\n' "$binary_json" | grep -q 'binary file changes skipped: 1'
 printf '%s\n' "$binary_json" | grep -q 'no added UTF-8 patch lines available'
 
+shallow_repo="$shallow/repo"
+git clone -q --depth 1 "file://$fixture" "$shallow_repo"
+shallow_json=$("$root/bin/jerboa-aigit" scan "$shallow_repo" --format json --count 1)
+printf '%s\n' "$shallow_json" | grep -q 'repository is shallow; parent history may be unavailable'
+
 if "$root/bin/jerboa-aigit" scan "$fixture/nope" >/tmp/jerboa-aigit-invalid.out 2>&1; then
   echo "invalid repository path should fail" >&2
   exit 1