#!/usr/bin/env bash
# analyzer-gate.sh — turn an analyzer's output into a pass/fail verdict.
#
# The 2026-08-10 audit asked for staticcheck, govulncheck and deadcode
# (V-694). govulncheck needed no gate of this shape because it already
# reported zero after the toolchain bump. The other two do not: staticcheck
# reports 20 findings today and deadcode reports 11 unreachable symbols, and
# three of those eleven are deliberate. A target that fails on the first run
# is not a gate, it is a target nobody runs. So the accepted set is written
# down, and only what is NOT in it fails.
#
#   staticcheck ./... | scripts/analyzer-gate.sh staticcheck
#   deadcode -test ./... | scripts/analyzer-gate.sh deadcode
#
# The baseline is keyed on file, check id and message, never on line number.
# A key carrying a line number goes stale on the next edit above it and then
# reports moved findings as new ones, which trains the reader to ignore it.
# The cost of dropping the line is that two identical findings in one file
# share one key, so the second is accepted with the first. That is the right
# way round: the same check firing twice on the same file is one thing to fix.
#
# A baseline entry with no finding left also fails. Fixing something and
# leaving its entry behind is how the accepted set stops describing the repo.
# The fix is one line: delete the entry the failure names.
#
# Reads stdin, writes a report, never writes a file.

set -uo pipefail
cd "$(dirname "$0")/.." || exit 1

tool="${1:?usage: analyzer-gate.sh <staticcheck|deadcode>}"
baseline="scripts/analyzers/$tool.baseline"
[ -f "$baseline" ] || { printf 'analyzer-gate: no baseline at %s\n' "$baseline" >&2; exit 2; }

# Normalise to "<file>\t<id>\t<message>". Anything that does not parse is an
# analyzer error, not a finding, and it fails without consulting the baseline.
# staticcheck: path.go:12:34: message (SA1234)
# deadcode:    path.go:12:34: unreachable func: Symbol
found=$(mktemp) || exit 2
malformed=$(mktemp) || exit 2
trap 'rm -f "$found" "$malformed"' EXIT

while IFS= read -r line; do
  [ -n "$line" ] || continue
  case "$tool" in
    staticcheck)
      if [[ "$line" =~ ^([^:]+):[0-9]+:[0-9]+:\ (.*)\ \(([A-Z]+[0-9]+)\)$ ]]; then
        # SA1019 ends its message with a space. Trim, so no baseline entry
        # depends on trailing whitespace surviving an editor.
        msg="${BASH_REMATCH[2]}"
        printf '%s\t%s\t%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[3]}" "${msg%"${msg##*[![:space:]]}"}" >>"$found"
      else
        printf '%s\n' "$line" >>"$malformed"
      fi
      ;;
    deadcode)
      if [[ "$line" =~ ^([^:]+):[0-9]+:[0-9]+:\ unreachable\ func:\ (.*)$ ]]; then
        printf '%s\tunreachable\t%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" >>"$found"
      else
        printf '%s\n' "$line" >>"$malformed"
      fi
      ;;
    *) printf 'analyzer-gate: unknown tool %s\n' "$tool" >&2; exit 2 ;;
  esac
done

if [ -s "$malformed" ]; then
  printf '%s: the analyzer said something that is not a finding:\n' "$tool" >&2
  sed 's/^/  /' "$malformed" >&2
  exit 1
fi

accepted=$(mktemp) || exit 2
trap 'rm -f "$found" "$malformed" "$accepted"' EXIT
grep -v '^[[:space:]]*\(#\|$\)' "$baseline" | sort -u >"$accepted"
sort -u "$found" -o "$found"

new=$(comm -23 "$found" "$accepted")
gone=$(comm -13 "$found" "$accepted")
status=0

if [ -n "$new" ]; then
  printf '%s: %d finding(s) not in %s:\n' "$tool" "$(printf '%s\n' "$new" | wc -l)" "$baseline"
  printf '%s\n' "$new" | sed 's/^/  /'
  printf 'Fix it, or add the line to the baseline with the reason it stays.\n'
  status=1
fi

if [ -n "$gone" ]; then
  printf '%s: %d baseline entry/entries no longer found:\n' "$tool" "$(printf '%s\n' "$gone" | wc -l)"
  printf '%s\n' "$gone" | sed 's/^/  /'
  printf 'Delete them from %s.\n' "$baseline"
  status=1
fi

[ "$status" -eq 0 ] && printf '%s: clean against %d accepted finding(s)\n' "$tool" "$(wc -l <"$accepted")"
exit "$status"
