#!/usr/bin/env bash
# Boundary tripwire: flag domain-taxonomy leaks in the Svelte frontend.
#
# Implements DR-094 (see docs/requirements.md).
#
# The project rule (CLAUDE.md, docs/architecture/02-svelte-frontend.md) is that
# the frontend is presentation-only and the Rust backend owns domain logic —
# including Jellyfin's item-type *taxonomy* (what the category "Music" means as a
# set of item types). See docs/specs/scoped-search-boundary.md for the incident
# that motivated this check.
#
# ⚠️ This is a TRIPWIRE, NOT A PROOF. A grep cannot distinguish taxonomy-as-policy
# (a leak) from taxonomy-as-display (legitimate: "is this a music card?"). It
# targets the machine-detectable signature of the leak class and defers
# everything subtler to the human spec-review checklist
# (docs/specs/SPEC-REVIEW-CHECKLIST.md). A clean run here does not mean the
# boundary is respected; it means the crudest violation isn't present.
#
# What it flags: an array literal naming two or more Jellyfin item types,
# ANYWHERE in src/ — i.e. the frontend deciding that a *category* maps to a *set*
# of Jellyfin types, which is domain knowledge the backend should own.
# Single-type arrays (`includeItemTypes: ["Movie"]`) are a page saying "I show
# movies" and are allowed. Type *inspection* (`item.type === "Audio"`) is display
# logic and is not matched.
#
# 🔴 What it still CANNOT see (do not read a green run as proof):
#   - a type set built at run time:      [...musicTypes, "Playlist"]
#   - types split across variables:      const A = "Audio"; [A, B]
#   - taxonomy as control flow:          switch (t) { case "Audio": … }
#                                        t === "Audio" || t === "MusicAlbum"
#   - an item type absent from ITEM_TYPES below (false negative by design)
#
# This check was hardened in July 2026 after the audit found it passing on the
# very leak it was written for: the original pattern was anchored to
# `includeItemTypes:` at the query site, so assigning the same array to a named
# const evaded it entirely. See docs/specs/boundary-tripwire-hardening.md (DR-094).
#
# Escaping a genuine exception: add the file+reason to the ALLOWLIST below.

set -euo pipefail

cd "$(dirname "$0")/.."

# Files permitted to contain a multi-type item-type array, with the reason.
# Keep this SHORT. A growing allowlist means the boundary is eroding — that is a
# signal to push taxonomy into Rust, not to keep appending here.
ALLOWLIST=(
  # "Things a person appeared in" is arguably taxonomy, but it is a fixed
  # two-type filmography query with no category-configuration behind it. Tracked
  # as acceptable pending any person-scope work; revisit if it grows.
  "src/lib/components/library/PersonDetailView.svelte"

  # Grid styling predicate over `config.itemType`, a value the page already
  # declares about itself. Selects a *look*, issues no query, and would only
  # change if the UI were redesigned — presentation, not taxonomy-as-policy.
  "src/lib/components/library/GenericMediaListPage.svelte"

  # "Is this item a container?" predicate for downloads browsing.
  # BORDERLINE — leans domain: the container set grows when Jellyfin adds a
  # container type. TODO: replace with a backend-supplied `MediaItem.isContainer`
  # flag and remove this entry. Tracked in
  # docs/specs/boundary-tripwire-hardening.md §Out of scope.
  "src/lib/components/downloads/DownloadedBrowse.svelte"
)

# Hard cap so erosion is caught mechanically rather than by whoever notices.
# Deliberately just above the current count: the next exception forces a
# conversation instead of a one-line append.
MAX_ALLOWLIST=4

if [[ "${#ALLOWLIST[@]}" -gt "$MAX_ALLOWLIST" ]]; then
  echo "❌ Allowlist has ${#ALLOWLIST[@]} entries (max $MAX_ALLOWLIST)."
  echo "   Push taxonomy into Rust instead of appending here."
  exit 1
fi

is_allowed() {
  local file="$1"
  for allowed in "${ALLOWLIST[@]}"; do
    [[ "$file" == "$allowed" ]] && return 0
  done
  return 1
}

# Two or more adjacent Jellyfin item-type string literals inside a bracket.
#
# NOT anchored to `includeItemTypes:` — that was the original rule, and it missed
# the real leak: `searchScope.ts` assigned the same array to a named const and
# dereferenced it one indirection away from the query, so the grep never saw it
# while CI stayed green. Matching the array literal itself catches a const, a
# Record value, a function return, and an inline query alike.
#
# Deliberate limits:
#   - requires TWO adjacent types, so single-type presentation
#     (`itemType: "Movie"`) stays legal — the rule targets *category* taxonomy;
#   - requires string literals, so `item.type === "Audio"` (display inspection)
#     does not match;
#   - uses an explicit type list rather than a generic capitalised-word pattern,
#     so unrelated string arrays (`["High","Low"]`) produce no noise.
#
# An item type missing from this list is a false *negative*, never a false
# positive — the check degrades safely as Jellyfin adds types.
ITEM_TYPES='Movie|Series|Episode|Audio|MusicAlbum|MusicArtist|MusicVideo|Season|BoxSet|Playlist|Book|AudioBook|Video|Person|Folder|CollectionFolder|TvChannel|LiveTvChannel'
PATTERN="\[[[:space:]]*\"($ITEM_TYPES)\"[[:space:]]*,[[:space:]]*\"($ITEM_TYPES)\""

echo "🔎 Checking frontend for domain-taxonomy leaks (item-type array literals)…"

# Collect hits, excluding tests and the allowlist.
violations=""
while IFS= read -r line; do
  [[ -z "$line" ]] && continue
  file="${line%%:*}"
  case "$file" in
    *.test.*) continue ;;
  esac
  if is_allowed "$file"; then
    echo "  ⏭️  allowlisted: $line"
    continue
  fi
  violations+="$line"$'\n'
done < <(grep -rInE "$PATTERN" src/ 2>/dev/null || true)

if [[ -n "$violations" ]]; then
  echo ""
  echo "❌ Frontend boundary violation: an item-type array literal defines a"
  echo "   category in the presentation layer. That taxonomy belongs in Rust —"
  echo "   send an opaque scope/enum and let the backend expand it to item types"
  echo "   (see SearchScope::item_types() in src-tauri/src/repository/types.rs)."
  echo "   Assigning the array to a const does not make it presentation."
  echo "   See docs/specs/scoped-search-boundary.md and CLAUDE.md."
  echo ""
  echo "$violations" | sed 's/^/   /'
  echo "   If this is a genuine exception, add the file + reason to ALLOWLIST in"
  echo "   scripts/check-frontend-boundary.sh — but prefer moving it to Rust."
  exit 1
fi

echo "✅ No multi-type taxonomy queries in the frontend."
echo "   (Reminder: this is a tripwire, not a proof — the spec-review checklist is"
echo "    the real gate for subtler leaks.)"
