Match duplicate candidates literally, not as a regex
Artist and title strings are full of regex metacharacters, so a new candidate could match an unrelated history entry and be silently skipped as already seen.
This commit is contained in:
@@ -15,6 +15,20 @@
|
|||||||
# Currently paired with: Arrs_Stack/playback_aware_lidarr_discovery.sh
|
# Currently paired with: Arrs_Stack/playback_aware_lidarr_discovery.sh
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
# OPERATIONAL MODEL
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# A library, not a program. Consumers source it and drive the pipeline themselves:
|
||||||
|
#
|
||||||
|
# 1. score_candidate() — sum the four weighted component scores
|
||||||
|
# 2. apply_temporal_decay() — reduce by one unit per 30 days of age, floored at 0
|
||||||
|
# 3. is_duplicate_candidate() — check the consumer's own history file
|
||||||
|
# 4. make_decision() — ACCEPT or REJECT against the consumer's threshold
|
||||||
|
#
|
||||||
|
# Every input is supplied by the caller and every output is returned to it. The engine holds
|
||||||
|
# no state between calls, reads no config, and never acts on its own verdict.
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
# DESIGN PRINCIPLES
|
# DESIGN PRINCIPLES
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
#
|
#
|
||||||
@@ -38,6 +52,60 @@
|
|||||||
# the consumer's concern — the engine never acts on its own verdict.
|
# the consumer's concern — the engine never acts on its own verdict.
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
# OPERATIONAL SAFEGUARDS
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# No Side Effects — Structural, Not Incidental
|
||||||
|
# Writes no files, makes no API calls, deletes nothing, starts nothing. This is the
|
||||||
|
# safeguard: a scoring mistake here can only ever produce a wrong number, never a wrong
|
||||||
|
# action. Keep it that way — the moment this library acquires a side effect, every consumer
|
||||||
|
# inherits it silently.
|
||||||
|
#
|
||||||
|
# No Root, No Lock, No detect_hosts — Deliberate
|
||||||
|
# Correct for a sourced library and should not be "fixed" to match the executable scripts.
|
||||||
|
# There is no state for a lock to protect, no privileged operation to gate, and no
|
||||||
|
# host-specific config to alias. It runs entirely inside the caller's process.
|
||||||
|
#
|
||||||
|
# Caller Owns the Verdict
|
||||||
|
# ACCEPT/REJECT is a return value, not an instruction. Nothing here can cause a candidate
|
||||||
|
# to be added, removed, or downloaded — the consumer decides what a verdict means.
|
||||||
|
#
|
||||||
|
# Literal Duplicate Matching
|
||||||
|
# is_duplicate_candidate() matches with grep -Fx, treating the candidate as data rather
|
||||||
|
# than a pattern. Artist and title strings routinely contain regex metacharacters, and a
|
||||||
|
# false positive here silently discards a genuinely new candidate.
|
||||||
|
#
|
||||||
|
# Decay Floors at Zero
|
||||||
|
# apply_temporal_decay() clamps at 0, so an old signal can never become a negative score
|
||||||
|
# that drags an otherwise-passing candidate below threshold.
|
||||||
|
#
|
||||||
|
# Integer Arithmetic Throughout
|
||||||
|
# All scoring is integer. No floating point means no locale-dependent decimal parsing and
|
||||||
|
# no rounding drift between hosts.
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
|
# CONFIGURATION
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# None, by design. The engine reads no conf file and no environment variable.
|
||||||
|
#
|
||||||
|
# Thresholds, weights and strictness profiles live with the consumer — see
|
||||||
|
# LIDARR_DISCOVERY_* / SONARR_DISCOVERY_* / RADARR_DISCOVERY_* in master.conf. That is what
|
||||||
|
# keeps the core domain-agnostic: music strictness cannot leak into TV intake, because the
|
||||||
|
# engine never learns which domain it is scoring for.
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
|
# RUNTIME MODES
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# None — this file is sourced, never executed:
|
||||||
|
#
|
||||||
|
# source "$SCRIPT_DIR/../Kernel/decision_engine.sh"
|
||||||
|
#
|
||||||
|
# It has no argument parsing, no --dry-run and no --status, because it takes no action that
|
||||||
|
# a dry run could suppress.
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
# FUNCTIONS
|
# FUNCTIONS
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
#
|
#
|
||||||
@@ -111,7 +179,12 @@ is_duplicate_candidate() {
|
|||||||
local candidate="$1"
|
local candidate="$1"
|
||||||
local history_file="$2"
|
local history_file="$2"
|
||||||
|
|
||||||
grep -qi "^${candidate}$" "$history_file" 2>/dev/null
|
# -F -x, not "^$" anchors: the candidate is data, not a pattern. Interpolating it into a
|
||||||
|
# regex makes every metacharacter in an artist or title active — "R.E.M." matches "RxExMy",
|
||||||
|
# and an unbalanced bracket makes grep error out entirely. Either way the caller reads the
|
||||||
|
# result as "already seen" and silently skips something genuinely new. -F disables regex,
|
||||||
|
# -x anchors the whole line, which is exactly what the anchors were reaching for.
|
||||||
|
grep -qiFx -- "$candidate" "$history_file" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── make_decision ─────────────────────────────────────────────────────────────
|
# ── make_decision ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user