#!/bin/sh
# ProAuditMap Qualitätsnachweis: unabhängige Prüfung ohne ProAuditMap.
# Benötigt: sh, openssl (1.1.1 oder 3.x, kein LibreSSL), sha256sum oder shasum, awk, grep, cut, cmp.
# Aufruf:   sh verify.sh [--previous <Ordner eines früheren Exports>]
# Exitcode: 0 alles verankert, 1 Fehler, 2 ohne Fehler, aber teilweise nicht verankert.
set -u
LC_ALL=C
export LC_ALL

cd "$(dirname "$0")" || exit 1
fail=0
partial=0
TAB=$(printf '\t')
say() { printf '%s\n' "$*"; }
err() { say "FEHLER: $*"; fail=1; }
warn() { say "HINWEIS: $*"; partial=1; }

previous=""
if [ "${1:-}" = "--previous" ]; then
  previous=${2:-}
  [ -d "$previous" ] || { say "FEHLER: --previous braucht einen Ordner eines früheren Exports"; exit 1; }
fi

# Schritt 1: openssl und Wurzelzertifikate
v=$(openssl version 2>/dev/null) || { say "FEHLER: openssl nicht gefunden"; exit 1; }
case "$v" in
  LibreSSL*)
    say "FEHLER: $v kann ts -verify nicht zuverlässig. Bitte OpenSSL 1.1.1 oder 3.x verwenden (macOS: brew install openssl)."
    exit 1
    ;;
esac
if command -v sha256sum >/dev/null 2>&1; then SHA="sha256sum"; else SHA="shasum -a 256"; fi
hash() { $SHA "$1" | cut -d' ' -f1; }

tmp=$(mktemp -d) || exit 1
trap 'rm -rf "$tmp"' EXIT
mkdir "$tmp/capath" "$tmp/pf"

say "Wurzelzertifikate (bitte mit der Veröffentlichung des Anbieters vergleichen, siehe README):"
awk -v d="$tmp" '/BEGIN CERT/{n++} n{print > (d "/root_" n ".pem")}' tsa/roots.pem
for f in "$tmp"/root_*.pem; do
  [ -f "$f" ] || { err "tsa/roots.pem enthält kein Zertifikat"; break; }
  openssl x509 -in "$f" -noout -subject -fingerprint -sha256
done

# Schritt 2: SHA256SUMS
if [ -f SHA256SUMS ]; then
  if $SHA -c SHA256SUMS >"$tmp/sums.out" 2>&1; then
    say "SHA256SUMS: alle Dateien stimmen."
  else
    err "SHA256SUMS stimmt nicht: $(grep -v ': OK$' "$tmp/sums.out" | head -n 5 | tr '\n' ' ')"
  fi
else
  err "SHA256SUMS fehlt"
fi

# Tage seit 1970 aus Jahr, Monat, Tag (reine Ganzzahlrechnung, Algorithmus days_from_civil)
days() {
  awk -v y="$1" -v m="$2" -v d="$3" 'BEGIN{ y -= (m <= 2); era = int((y >= 0 ? y : y - 399) / 400); yoe = y - era * 400; mp = (m + 9) % 12; doy = int((153 * mp + 2) / 5) + d - 1; doe = yoe * 365 + int(yoe / 4) - int(yoe / 100) + doy; print era * 146097 + doe - 719468 }'
}
mon() {
  case "$1" in Jan) echo 1;; Feb) echo 2;; Mar) echo 3;; Apr) echo 4;; May) echo 5;; Jun) echo 6;; Jul) echo 7;; Aug) echo 8;; Sep) echo 9;; Oct) echo 10;; Nov) echo 11;; Dec) echo 12;; *) echo 0;; esac
}
norm() { printf '%s' "$1" | tr -s ' '; }

# Schritt 3: Zeitstempel je Anker und Token
# Ein Token, das nicht besteht, ist nur ein Hinweis, solange ein anderes Token desselben Ankerdokuments besteht
# (Spec 8a); bestehen alle nicht, meldet Schritt 4 den fehlenden Zeitstempel als FEHLER.
: >"$tmp/anchor_time"
: >"$tmp/tok_fail"
tokfail() { printf '%s\t%s\n' "$1" "$2" >>"$tmp/tok_fail"; }
if [ ! -f anchors.tsv ]; then
  err "anchors.tsv fehlt"
else
  while IFS="$TAB" read -r kind day file tok unix text; do
    [ "$kind" = "anchor" ] || continue
    out=$(openssl ts -verify -data "anchors/$file" -in "anchors/$tok" -CAfile tsa/roots.pem -CApath "$tmp/capath" -untrusted tsa/intermediates.pem -attime "$unix" 2>&1 </dev/null)
    case "$out" in
      *"Verification: OK"*) ;;
      *) tokfail "$file" "Zeitstempel $tok zu anchors/$file nicht verifizierbar"; continue ;;
    esac
    line=$(openssl ts -reply -in "anchors/$tok" -text 2>/dev/null </dev/null | grep '^Time stamp:')
    rest=$(printf '%s' "${line#Time stamp:}" | tr -s ' ' | sed 's/^ //')
    want=$(printf '%s' "$text" | tr -s ' ' | sed 's/^ //')
    if [ "$rest" != "$want" ]; then
      tokfail "$file" "genTime von $tok weicht vom Manifest ab ($rest gegenüber $want)"
      continue
    fi
    # shellcheck disable=SC2086
    set -- $rest
    m=$(mon "$1")
    d=$2
    y=$4
    gd=$(days "$y" "$m" "$d")
    if [ "$gd" -ne $((unix / 86400)) ]; then
      tokfail "$file" "Kalendertag von genTime in $tok passt nicht zur Unixzeit $unix"
      continue
    fi
    # Ankertag aus dem Namen YYYY-MM-DD
    ay=${day%%-*}; rest2=${day#*-}; am=${rest2%%-*}; ad_=${rest2#*-}
    ad=$(days "$ay" "$am" "$ad_")
    if [ "$gd" -lt "$ad" ] || [ "$gd" -gt $((ad + 2)) ]; then
      tokfail "$file" "genTime von $tok liegt nicht am Ankertag $day oder höchstens zwei Tage danach: ein spät gestempelter Anker belegt nichts für den genannten Tag"
      continue
    fi
    printf '%s\t%s\t%s\n' "$file" "$unix" "$rest" >>"$tmp/anchor_time"
  done <anchors.tsv
fi
while IFS="$TAB" read -r file msg; do
  if awk -F"$TAB" -v f="$file" '$1==f{found=1} END{exit !found}' "$tmp/anchor_time"; then
    say "HINWEIS: $msg (ein anderes Token zu anchors/$file besteht)"
  else
    err "$msg"
  fi
done <"$tmp/tok_fail"

# Schritt 4: Segmente der Hash-Kette bis zum verankerten Kopf
: >"$tmp/index.tsv"
: >"$tmp/seg_state"
split_pre() {
  # Zerlegt eine Hash-Eingabe in ihre Felder 0 (Kettenversion) und 1 bis 9 (prev_hash, id, occurredAt, actor,
  # action, entityType, entityId, details, reason); je Feld eine Datei in $2.
  awk -v d="$2" 'BEGIN{RS="\001"} { s=$0; p=index(s,"|zeile"); f=d "/0"; printf "%s", substr(s,1,p-1) > f; close(f); s=substr(s,p+6);
    for(i=1;i<=9;i++){
      if(substr(s,1,1)=="-"){ v=""; s=substr(s,2) } else { c=index(s,":"); n=substr(s,1,c-1)+0; v=substr(s,c+1,n); s=substr(s,c+1+n) }
      f=d "/" i; printf "%s", v > f; close(f) } }' "$1"
}
field() { cat "$tmp/pf/$1"; }
ishex64() {
  case "$1" in ''|*[!0-9a-f]*) return 1 ;; esac
  [ "${#1}" -eq 64 ]
}
isnum() {
  case "$1" in ''|*[!0-9]*) return 1 ;; esac
  return 0
}

if [ ! -f segments.tsv ] || [ ! -f chain/rows.tsv ]; then
  err "segments.tsv oder chain/rows.tsv fehlt"
else
  segno=0
  while IFS="$TAB" read -r from to day file lasthash; do
    segno=$((segno + 1))
    if ! isnum "$from" || ! isnum "$to" || ! ishex64 "$lasthash"; then err "segments.tsv: ungültige Angaben in Segment $segno"; continue; fi
    awk -F"$TAB" -v f="$from" -v t="$to" 'NR>1 && $1+0>=f && $1+0<=t' chain/rows.tsv >"$tmp/seg.tsv"
    if [ ! -s "$tmp/seg.tsv" ]; then err "Segment $from bis $to enthält keine Zeilen"; continue; fi
    # Spalten aus rows.tsv sind nicht vertrauenswürdig: jede Zeile, die nicht ausdrücklich exportierbar (1) ist, sperrt
    # alle Zeilen davor, unabhängig von der Spalte own.
    lastblocked=$(awk -F"$TAB" '$5!="1"{b=NR} END{print b+0}' "$tmp/seg.tsv")
    lastid=""; lastrow=""; lastprev=""; lastred=0; redids=""; prevmark=start; n=0
    while IFS="$TAB" read -r id prev rowh own exp red; do
      n=$((n + 1))
      # Datenschutz: Zuweisungs- und Abmeldezeilen (und ihre Folgezeile) tragen statt der Hashes ein '-', aber nur mit genau dieser Markierung in der Spalte redact.
      # Jede andere Form (fehlender oder ungültiger Hash ohne Markierung, unbekannte Markierung) ist ein Fehler.
      case "${red:-0}" in
        datenschutz)
          if ! isnum "$id" || [ "$prev" != "-" ] || [ "$rowh" != "-" ] || [ "$exp" != "0" ]; then err "chain/rows.tsv: ungültige Datenschutz-Markierung in Segment $from bis $to (Zeile $n)"; continue; fi
          case "$own" in 0|1) ;; *) err "chain/rows.tsv: ungültiges Feld own in Zeile $id" ;; esac
          [ ! -f "chain/preimages/$id.txt" ] || err "Zeile $id ist nicht exportierbar, ihre Hash-Eingabe liegt dennoch bei"
          lastid=$id; lastrow=""; lastred=1; redids="$redids $id"; prevmark=datenschutz
          continue
          ;;
        datenschutz-folge)
          if ! isnum "$id" || [ "$prev" != "-" ] || ! ishex64 "$rowh" || [ "$exp" != "0" ]; then err "chain/rows.tsv: ungültige Datenschutz-Markierung in Segment $from bis $to (Zeile $n)"; continue; fi
          case "$own" in 0|1) ;; *) err "chain/rows.tsv: ungültiges Feld own in Zeile $id" ;; esac
          [ ! -f "chain/preimages/$id.txt" ] || err "Zeile $id ist nicht exportierbar, ihre Hash-Eingabe liegt dennoch bei"
          # datenschutz-folge nur als erste Zeile des Segments oder direkt nach datenschutz
          case "$prevmark" in start|datenschutz) ;; *) err "chain/rows.tsv: Markierung datenschutz-folge an unzulässiger Stelle (Zeile $id)" ;; esac
          lastid=$id; lastrow=$rowh; lastred=0; redids="$redids $id"; prevmark=folge
          continue
          ;;
        0)
          # nach datenschutz muss datenschutz oder datenschutz-folge folgen
          [ "$prevmark" != "datenschutz" ] || err "chain/rows.tsv: nach einer Zeile mit Markierung datenschutz fehlt datenschutz-folge (Zeile $id)"
          prevmark=0
          ;;
        *) err "chain/rows.tsv: unbekannte Markierung in Zeile $id"; continue ;;
      esac
      if ! isnum "$id" || ! ishex64 "$prev" || ! ishex64 "$rowh"; then err "chain/rows.tsv: ungültige Angaben in Segment $from bis $to (Zeile $n)"; continue; fi
      case "$own" in 0|1) ;; *) err "chain/rows.tsv: ungültiges Feld own in Zeile $id" ;; esac
      case "$exp" in 0|1) ;; *) err "chain/rows.tsv: ungültiges Feld exportable in Zeile $id"; lastid=$id; lastrow=$rowh; continue ;; esac
      if [ -n "$lastrow" ] && [ "$prev" != "$lastrow" ]; then err "Zeile $id zeigt nicht auf die Vorzeile (Kette bricht in Segment $from bis $to)"; fi
      lastid=$id; lastrow=$rowh; lastred=0
      pre="chain/preimages/$id.txt"
      if [ "$exp" = "1" ]; then
        if [ ! -f "$pre" ]; then err "Hash-Eingabe $pre fehlt"; continue; fi
        h=$(hash "$pre")
        if [ "$h" != "$rowh" ]; then err "Hash-Eingabe von Zeile $id passt nicht zu ihrem Zeilenhash"; continue; fi
        split_pre "$pre" "$tmp/pf"
        [ "$(field 0)" = "auditplan-audit-kette-v1" ] || err "Zeile $id: unbekannte Kettenversion"
        [ "$(field 1)" = "$prev" ] || err "Zeile $id: prev_hash in der Hash-Eingabe weicht von rows.tsv ab"
        [ "$(field 2)" = "$id" ] || err "Zeile $id: id in der Hash-Eingabe weicht ab"
        state=ok
        if [ "$n" -lt "$lastblocked" ]; then
          state=offline
          [ "$own" = "1" ] && warn "Zeile $id ist nicht offline verankert (eine nicht exportierbare Zeile folgt im selben Segment)"
        fi
        field 8 >"$tmp/det_$id"
        printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$id" "$state" "$(field 5)" "$(field 6)" "$(field 7)" "$file" >>"$tmp/index.tsv"
      else
        [ ! -f "$pre" ] || err "Zeile $id ist nicht exportierbar, ihre Hash-Eingabe liegt dennoch bei"
      fi
    done <"$tmp/seg.tsv"
    if [ -n "$redids" ]; then warn "Zeilen$redids (Segment $from bis $to): offline nicht prüfbar (Datenschutz); die Kette ist dort nicht lückenlos nachrechenbar"; fi
    if [ "$lastid" != "$to" ]; then
      err "Segment endet nicht am verankerten Kopf (Zeile $to)"
    elif [ "$lastred" = "1" ]; then
      err "Segment endet auf einer Zeile mit zurückgehaltenem Hash: der verankerte Kopf (Zeile $to) darf nie geschwärzt sein"
    elif [ "$lastrow" != "$lasthash" ]; then
      err "Segment endet nicht am verankerten Kopf (Zeile $to)"
    fi
    if [ ! -f "anchors/$file" ]; then err "Ankerdokument anchors/$file fehlt"; continue; fi
    block=$(tr -d ' \n\t\r' <"anchors/$file" | sed -n 's/.*"chain":{\([^}]*\)}.*/\1/p')
    case "$block" in *'"ok":true'*) ;; *) err "Das Ankerdokument anchors/$file meldet eine auffällige Kette" ;; esac
    case "$block" in *"\"lastId\":\"$to\""*) ;; *) err "Der Kopf in anchors/$file nennt nicht die Zeile $to" ;; esac
    case "$block" in *"\"lastHash\":\"$lasthash\""*) ;; *) err "Der Kopf in anchors/$file nennt nicht den Hash der Zeile $to" ;; esac
    if ! awk -F"$TAB" -v f="$file" '$1==f{found=1} END{exit !found}' "$tmp/anchor_time"; then err "Für anchors/$file liegt kein gültiger Zeitstempel vor"; fi
  done <segments.tsv
fi

earliest() {
  # Früheste genTime-Textform der gültigen Token zu einem Ankerdokument
  sort -t "$TAB" -k2,2n "$tmp/anchor_time" | awk -F"$TAB" -v f="$1" '$1==f{print $3; exit}'
}

# Schritt 5 und 6: Versionen und Freigaben im verankerten Protokoll
audit=$(awk -F"$TAB" '$1=="audit"{print $2; exit}' meta.tsv 2>/dev/null)
[ -n "$audit" ] || err "meta.tsv nennt keine Prüfung"
say "Prüfung laut meta.tsv: $audit (bitte mit der erwarteten Prüfung vergleichen)"
tenants=""
for a in anchors/*.json; do
  [ -f "$a" ] || continue
  t=$(tr -d ' \n\t\r' <"$a" | sed -n 's/.*"tenant":{"id":"\([^"]*\)".*/\1/p')
  tenants="$tenants $t"
done
uniq_tenants=$(printf '%s\n' $tenants | sort -u)
case "$uniq_tenants" in
  '') err "Die Ankerdokumente nennen keinen Mandanten" ;;
  *"
"*) err "Die Ankerdokumente nennen verschiedene Mandanten: $(printf '%s' "$uniq_tenants" | tr '\n' ' ')" ;;
  *) say "Mandant laut Ankerdokument: $uniq_tenants (bitte mit dem erwarteten Mandanten vergleichen)" ;;
esac
prove() {
  # $1 Bezeichnung, $2 Aktion, $3 Entitätstyp, $4 Entitäts-ID, $5 Suchtext in den Details, $6 weiterer Suchtext (oder leer)
  label=$1
  rows=$(awk -F"$TAB" -v a="$2" -v t="$3" -v e="$4" '$3==a && $4==t && $5==e {print $1 "\t" $2 "\t" $6}' "$tmp/index.tsv")
  if [ -z "$rows" ]; then warn "$label: nicht verankert"; return; fi
  hit=0
  while IFS="$TAB" read -r id state file; do
    if grep -Fq "$5" "$tmp/det_$id" && { [ -z "$6" ] || grep -Fq "$6" "$tmp/det_$id"; }; then
      if [ "$state" = "ok" ]; then say "$label: spätestens am $(earliest "$file") verankert"; else warn "$label: nicht offline verankert"; fi
      hit=1
      break
    fi
  done <<EOF
$rows
EOF
  [ "$hit" -eq 1 ] || err "$label: der Inhalt passt zu keiner verankerten Protokollzeile"
}
prove_review() {
  # $1 Feststellung, $2 Review, $3 reviewter Inhaltshash, $4 Selbstreview (0 oder 1), $5 Bezeichnung
  rrows=$(awk -F"$TAB" -v e="$1" '$3=="finding.reviewed" && $4=="finding" && $5==e {print $1 "\t" $2 "\t" $6}' "$tmp/index.tsv")
  if [ -z "$rrows" ]; then
    # Ohne Kettenzeile ist das Review nur dann ein Hinweis, wenn das Paket Zeilen nach dem letzten Anker nennt; sonst fehlt der Beleg.
    if grep -q "^unanchored$TAB" meta.tsv; then warn "$5: nicht verankert"; else err "$5: keine verankerte Protokollzeile"; fi
    return
  fi
  rhit=0
  while IFS="$TAB" read -r id state file; do
    if grep -Fq "\"reviewId\":\"$2\"" "$tmp/det_$id"; then
      rhit=1
      if ! grep -Fq "\"contentSha256\":\"$3\"" "$tmp/det_$id"; then err "$5: der reviewte Inhaltshash passt nicht zur verankerten Protokollzeile"; break; fi
      if grep -Fq '"selfReview":true' "$tmp/det_$id"; then chainflag=1; else chainflag=0; fi
      if [ "$chainflag" != "$4" ]; then err "$5: das Selbstreview-Kennzeichen passt nicht zur verankerten Protokollzeile"; break; fi
      if [ "$state" = "ok" ]; then say "$5: spätestens am $(earliest "$file") verankert"; else warn "$5: nicht offline verankert"; fi
      break
    fi
  done <<EOF
$rrows
EOF
  [ "$rhit" -eq 1 ] || err "$5: der Inhalt passt zu keiner verankerten Protokollzeile"
}
if [ -f meta.tsv ]; then
  while IFS="$TAB" read -r kind n path extra flag; do
    case "$kind" in
      finding)
        [ -f "$path" ] || err "$path fehlt"
        ;;
      fversion)
        [ -f "$path" ] || { err "$path fehlt"; continue; }
        fnum=$(basename "$path" | cut -c1-4 | sed 's/^0*//')
        fver=${extra%%:*}
        fhash=${extra#*:}
        grep -Fq "\"contentSha256\": \"$fhash\"" "$path" || err "Feststellung $fnum Version $fver: die Datei nennt den Hash aus meta.tsv nicht"
        prove "Feststellung $fnum Version $fver" finding.version_created finding "$n" "\"contentSha256\":\"$fhash\"" "\"version\":$fver,"
        ;;
      freview)
        [ -f "$path" ] || { err "$path fehlt"; continue; }
        rid=${extra%%:*}
        rhash=${extra#*:}
        grep -Fq "\"id\": \"$rid\"" "$path" || err "Review $rid: die Datei nennt die Kennung aus meta.tsv nicht"
        prove_review "$n" "$rid" "$rhash" "$flag" "Review $rid"
        ;;
      report)
        [ -f "$path" ] || err "$path fehlt"
        ;;
      rversion)
        [ -f "$path" ] || { err "$path fehlt"; continue; }
        rver=${extra%%:*}
        rhash=${extra#*:}
        grep -Fq "\"contentSha256\": \"$rhash\"" "$path" || err "Bericht Version $rver: die Datei nennt den Hash aus meta.tsv nicht"
        prove "Bericht Version $rver" report.version_created report "$n" "\"contentSha256\":\"$rhash\"" "\"version\":$rver,"
        ;;
      rfinal)
        [ -f "$path" ] || { err "$path fehlt"; continue; }
        rfid=${extra%%:*}
        rpdf=${extra#*:}
        grep -Fq "\"pdfSha256\": \"$rpdf\"" "$path" || err "Endbericht $rfid: die Datei nennt den PDF-Hash aus meta.tsv nicht"
        prove "Endbericht $rfid" report.finalized report "$n" "\"finalId\":\"$rfid\"" "\"pdfSha256\":\"$rpdf\""
        ;;
      version)
        [ -f "$path" ] || { err "$path fehlt"; continue; }
        prove "Version $n" package_version.created package_version "$audit:$n" "\"contentSha256\":\"$(hash "$path")\"" ""
        ;;
      release)
        [ -f "$path" ] || { err "$path fehlt"; continue; }
        prove "Freigabe Version $n" audit.status_changed audit "$audit" "\"snapshotSha256\":\"$(hash "$path")\"" '"to":"freigegeben"'
        ;;
      unanchored) warn "Zeile $n liegt nach dem letzten Anker und ist nicht verankert" ;;
    esac
  done <meta.tsv
  # Rückrichtung: jede verankerte Zeile der Feststellungen dieser Prüfung braucht einen Eintrag in meta.tsv (sonst fehlt sie im Paket)
  # Dasselbe für Berichtsversionen und Endberichte (8d-2): jede verankerte Zeile dieser Prüfung braucht ihren Eintrag in meta.tsv
  brows=$(awk -F"$TAB" '$3=="report.version_created" || $3=="report.finalized" {print $1 "\t" $3 "\t" $5}' "$tmp/index.tsv")
  if [ -n "$brows" ]; then
    while IFS="$TAB" read -r id action ent; do
      grep -Fq "\"auditId\":\"$audit\"" "$tmp/det_$id" || continue
      case "$action" in
        report.version_created)
          bver=$(sed -n 's/.*"version":\([0-9]*\),.*/\1/p' "$tmp/det_$id")
          awk -F"$TAB" -v e="$ent" -v v="$bver" '$1=="rversion" && $2==e && index($4, v ":")==1 {f=1} END{exit !f}' meta.tsv || err "Die verankerte Berichtsversion $bver fehlt in meta.tsv"
          ;;
        report.finalized)
          bfid=$(sed -n 's/.*"finalId":"\([^"]*\)".*/\1/p' "$tmp/det_$id")
          awk -F"$TAB" -v e="$ent" -v v="$bfid" '$1=="rfinal" && $2==e && index($4, v ":")==1 {f=1} END{exit !f}' meta.tsv || err "Der verankerte Endbericht $bfid fehlt in meta.tsv"
          ;;
      esac
    done <<EOF2
$brows
EOF2
  fi
  frows=$(awk -F"$TAB" '$3=="finding.reviewed" || $3=="finding.version_created" || $3=="finding.created" {print $1 "\t" $3 "\t" $5}' "$tmp/index.tsv")
  if [ -n "$frows" ]; then
    while IFS="$TAB" read -r id action ent; do
      grep -Fq "\"auditId\":\"$audit\"" "$tmp/det_$id" || continue
      case "$action" in
        finding.reviewed)
          rid=$(sed -n 's/.*"reviewId":"\([^"]*\)".*/\1/p' "$tmp/det_$id")
          awk -F"$TAB" -v r="$rid" '$1=="freview" && index($4, r ":")==1 {f=1} END{exit !f}' meta.tsv || err "Das verankerte Review $rid fehlt in meta.tsv"
          ;;
        finding.version_created)
          ver=$(sed -n 's/.*"version":\([0-9]*\),.*/\1/p' "$tmp/det_$id")
          awk -F"$TAB" -v e="$ent" -v v="$ver" '$1=="fversion" && $2==e && index($4, v ":")==1 {f=1} END{exit !f}' meta.tsv || err "Die verankerte Version $ver der Feststellung $ent fehlt in meta.tsv"
          ;;
        finding.created)
          awk -F"$TAB" -v e="$ent" '$1=="finding" && $2==e {f=1} END{exit !f}' meta.tsv || err "Die verankerte Feststellung $ent fehlt in meta.tsv"
          ;;
      esac
    done <<EOF
$frows
EOF
  fi
fi

# Schritt 7: Vergleich mit einem früheren Export
if [ -n "$previous" ]; then
  for f in anchors/*.json; do
    other="$previous/$f"
    if [ -f "$other" ]; then
      if cmp -s "$f" "$other"; then say "Anker $f stimmt mit dem früheren Export überein."; else err "Anker $f weicht vom früheren Export ab"; fi
    fi
  done
fi

if [ "$fail" -ne 0 ]; then
  say "ERGEBNIS: FEHLER"
  exit 1
fi
if [ "$partial" -ne 0 ]; then
  say "ERGEBNIS: ohne Fehler, aber teilweise nicht verankert"
  exit 2
fi
say "ERGEBNIS: alles geprüft und verankert"
exit 0
