diff --git a/.gitea/workflows/build-and-test.yml b/.gitea/workflows/build-and-test.yml index 1fbd334e..fae13e8a 100644 --- a/.gitea/workflows/build-and-test.yml +++ b/.gitea/workflows/build-and-test.yml @@ -49,6 +49,13 @@ jobs: run: | bun install + # Tripwire for domain-taxonomy leaks into the presentation layer (a + # multi-type includeItemTypes query defining a category in the frontend). + # See scripts/check-frontend-boundary.sh and + # docs/specs/scoped-search-boundary.md. + - name: Check frontend/backend boundary + run: bash scripts/check-frontend-boundary.sh + - name: Run frontend tests run: | bunx svelte-kit sync diff --git a/package.json b/package.json index 7ae171ab..e0f5ec95 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test:e2e:dev": "wdio run ./wdio.conf.ts --watch", "test:all": "./scripts/test-all.sh", "test:rust": "./scripts/test-rust.sh", + "check:boundary": "bash scripts/check-frontend-boundary.sh", "android:build": "./scripts/build-android.sh", "android:build:release": "./scripts/build-android.sh release", "android:build:clean": "rm -rf node_modules/.vite dist .svelte-kit .next build target src-tauri/target && bun install && bun run build", diff --git a/scripts/check-frontend-boundary.sh b/scripts/check-frontend-boundary.sh new file mode 100755 index 00000000..e24e1817 --- /dev/null +++ b/scripts/check-frontend-boundary.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Boundary tripwire: flag domain-taxonomy leaks in the Svelte frontend. +# +# 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 one machine-detectable signature of the leak class — a *query* that +# names a multi-type category — 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 `includeItemTypes: [ ... , ... ]` array literal with two or +# more types — 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 +# query 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. +# +# 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 includeItemTypes query, 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" +) + +is_allowed() { + local file="$1" + for allowed in "${ALLOWLIST[@]}"; do + [[ "$file" == "$allowed" ]] && return 0 + done + return 1 +} + +# Multi-element includeItemTypes array: `includeItemTypes: [ , ... ]`. +# The comma inside the brackets is what makes it multi-type. +PATTERN='includeItemTypes:[[:space:]]*\[[^]]*,[^]]*\]' + +echo "🔎 Checking frontend for domain-taxonomy leaks (multi-type query arrays)…" + +# 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: a multi-type includeItemTypes query defines" + echo " a category in the presentation layer. That taxonomy belongs in Rust —" + echo " send an opaque scope and let the backend expand it to item types." + 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.)"