A spec was a promise; sixteen of them had become descriptions of code that already shipped, sitting beside four that describe work still outstanding, with nothing in the file telling the two apart. Half the statuses were also wrong — audio-equalizer read "Accepted" with the EQ live on both platforms, the native video spec said the flag stays off after the default was flipped on. The shipped designs move into docs/architecture, which is the maintained description of the build, and the spec files go. Git history keeps the originals; what a future change still needs is carried across: - 01-rust-backend: favourites rewritten (the old section named a file that no longer exists and called shipped buttons "planned"), domain vocabulary owned by Rust (SearchScope, exclusions, the bitrate ladder), background workers - 02-svelte-frontend: app shell and chrome, library mosaic, series/episode navigation, downloaded browse, safe-area insets, native-video store, logging - 03-data-flow: locally-indexed search - 05-platform-backends: audio settings on ExoPlayer, the equalizer's band vocabulary, native video compositing, the background-audio handoff - 06-downloads-and-offline: one storage model, offline catalog visibility - 09-security: path confinement and input binding docs/specs/README.md now says what the directory is for and where each shipped design went. Deferred work the specs recorded is kept beside the code it concerns rather than lost: season-bounded autoplay, the two dead search commands, why indexing is a full crawl. requirements.md had fourteen stale statuses — Android audio parity still read "Linux only", DR-150 still said the native-video default was off, DR-190 was Proposed after DR-196 implemented it, and five tooling requirements were Proposed after landing. Three unbuilt specs suggested requirement ids that have since been allocated to other work; each now carries a warning.
142 lines
6.6 KiB
Bash
Executable File
142 lines
6.6 KiB
Bash
Executable File
#!/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 (DR-094). The pattern below is the hardened one: it
|
|
# matches an item-type array literal anywhere, not just at a query site.
|
|
#
|
|
# 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
|
|
# a backend-supplied flag; deferred rather than bundled with the tripwire work.
|
|
"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.)"
|