#!/usr/bin/env bash
# Tool-inventory parity sweep (NAROS.md §10.3): nothing agents rely on may silently
# vanish when the default sandbox image moves from nucleic-sandbox:v7 to naros-agent.
#
#   parity-sweep.sh <candidate-image> <reference-image> [--allow FILE]
#
# Compares (a) the set of executables on PATH and (b) the npm global package set, in
# each image. Anything present in the REFERENCE but missing from the CANDIDATE fails
# the sweep unless listed in the allowlist (deliberate, understood removals — one name
# per line, `#` comments). Additions are reported informationally. Python module parity
# is not swept: neither image bakes site-packages beyond pip's own.
#
# Exit: 0 parity holds, 1 unexplained removals, 2 usage/docker errors.
set -euo pipefail

CAND="${1:?usage: parity-sweep.sh <candidate-image> <reference-image> [--allow FILE]}"
REF="${2:?missing reference image}"
shift 2
ALLOW="$(cd "$(dirname "$0")" && pwd)/parity-allowlist.txt"
while [ $# -gt 0 ]; do
  case "$1" in
    --allow) ALLOW="$2"; shift 2 ;;
    *) echo "unknown arg: $1" >&2; exit 2 ;;
  esac
done

# Every executable name reachable on the image's default PATH. --entrypoint /bin/sh is
# nash in naros images and dash in the v7 base — the script is portable to both.
path_inventory() {
  docker run --rm --entrypoint /bin/sh "$1" -c \
    'for d in $(echo "$PATH" | tr : " "); do ls -1 "$d" 2>/dev/null; done | sort -u'
}

npm_inventory() {
  docker run --rm --entrypoint /bin/sh "$1" -c \
    'npm ls -g --depth=0 --parseable 2>/dev/null | tail -n +2' \
    | awk -F/ 'NF { if ($(NF-1) ~ /^@/) print $(NF-1)"/"$NF; else print $NF }' | sort -u
}

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

echo "inventorying candidate $CAND …"
path_inventory "$CAND" > "$tmp/cand.path"
npm_inventory "$CAND" > "$tmp/cand.npm"
echo "inventorying reference $REF …"
path_inventory "$REF" > "$tmp/ref.path"
npm_inventory "$REF" > "$tmp/ref.npm"

allow_filter() {
  if [ -f "$ALLOW" ]; then
    grep -vE '^\s*(#|$)' "$ALLOW" | grep -vxF -f /dev/stdin "$1" || true
  else
    cat "$1"
  fi
}

comm -23 "$tmp/ref.path" "$tmp/cand.path" > "$tmp/removed.path"
comm -23 "$tmp/ref.npm" "$tmp/cand.npm" > "$tmp/removed.npm"
added_path=$(comm -13 "$tmp/ref.path" "$tmp/cand.path" | wc -l)

cat "$tmp/removed.path" "$tmp/removed.npm" | sort -u > "$tmp/removed.all"
allow_filter "$tmp/removed.all" > "$tmp/unexplained" || true

echo "PATH: $(wc -l < "$tmp/ref.path") reference, $(wc -l < "$tmp/cand.path") candidate," \
     "$(wc -l < "$tmp/removed.path") removed, $added_path added"
echo "npm globals removed: $(wc -l < "$tmp/removed.npm")"

if [ -s "$tmp/unexplained" ]; then
  echo "PARITY FAILURE — present in $REF, missing from $CAND, not allowlisted:" >&2
  cat "$tmp/unexplained" >&2
  echo "(fix the image, or add to $ALLOW with a comment explaining the removal)" >&2
  exit 1
fi
if [ -s "$tmp/removed.all" ]; then
  echo "--- deliberate removals (allowlisted) ---"
  cat "$tmp/removed.all"
fi
echo "parity OK"
