feat(governance): enforce fail-closed verification, handle shortfall edge cases, pass 23/23 tests
HIFFI QA & Governance Pipeline / governance-tests (push) Successful in 24s
HIFFI QA & Governance Pipeline / governance-tests (push) Successful in 24s
This commit is contained in:
1 parent
0b36fff7b1
commit
f40143cb69
2 files changed
+134
-18
No files matched your search
+55
-17
@@ -233,25 +233,40 @@ class HIFFIRankingEngine:
|
|||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def apply_publication_eligibility_gates(self, df: pd.DataFrame) -> pd.DataFrame:
|
def apply_publication_eligibility_gates(
|
||||||
|
self,
|
||||||
|
df: pd.DataFrame,
|
||||||
|
return_metadata: bool = False
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Constructs a publication ranking layer separate from raw mathematical scores:
|
Constructs a publication ranking layer decoupled from raw scores:
|
||||||
- Top 10 requires all 3 platform signals (Tier 3).
|
- Top 10 requires all 3 platforms (Tier 3), strictly verified.
|
||||||
- Top 50 requires at least 2 platform signals (Tier >= 2).
|
- Top 50 requires at least 2 platforms (Tier >= 2), strictly verified.
|
||||||
- Preserves hiffi_xp_score and hiffi_xp_rank unchanged.
|
- Unverified or unindexed Instagram accounts do NOT contribute to evidence tiers.
|
||||||
- Produces consecutive, unique ranks in 'hiffi_xp_published_rank' (1 to N).
|
- Fail-closed: missing verification column evaluates strictly to False.
|
||||||
|
- Explicitly tracks shortfalls and degrades gracefully without rank gaps or duplicate integers.
|
||||||
"""
|
"""
|
||||||
out = df.copy()
|
out = df.copy()
|
||||||
|
|
||||||
# 1. Determine verified platform presence tier
|
# 1. Strict Signal Verification
|
||||||
has_yt = out["production_hiffi_score"].notna()
|
has_yt = out["production_hiffi_score"].notna()
|
||||||
has_sp = out["spotify_monthly_listeners"].notna()
|
has_sp = out["spotify_monthly_listeners"].notna()
|
||||||
has_ig = out["instagram_followers"].notna()
|
|
||||||
|
# Fail-closed Instagram verification check
|
||||||
|
verified_ig_statuses = {"AUTOMATED_VERIFIED", "MANUAL_VERIFIED"}
|
||||||
|
if "instagram_verification_status" in out.columns:
|
||||||
|
has_ig = (
|
||||||
|
out["instagram_followers"].notna()
|
||||||
|
& out["instagram_verification_status"].isin(verified_ig_statuses)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
has_ig = pd.Series(False, index=out.index)
|
||||||
|
|
||||||
out["platform_evidence_tier"] = (
|
out["platform_evidence_tier"] = (
|
||||||
has_yt.astype(int) + has_sp.astype(int) + has_ig.astype(int)
|
has_yt.astype(int) + has_sp.astype(int) + has_ig.astype(int)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Sort the entire pool deterministically by raw composite score
|
# 2. Sort pool deterministically by raw composite score
|
||||||
sort_cols = ["hiffi_xp_score", "production_hiffi_score", "spotify_monthly_listeners"]
|
sort_cols = ["hiffi_xp_score", "production_hiffi_score", "spotify_monthly_listeners"]
|
||||||
present_sort_cols = [c for c in sort_cols if c in out.columns]
|
present_sort_cols = [c for c in sort_cols if c in out.columns]
|
||||||
pool = out.sort_values(
|
pool = out.sort_values(
|
||||||
@@ -260,23 +275,44 @@ class HIFFIRankingEngine:
|
|||||||
).copy()
|
).copy()
|
||||||
|
|
||||||
assigned_indices = []
|
assigned_indices = []
|
||||||
|
shortfall_audit = {"top_10_shortfall": False, "top_50_shortfall": False}
|
||||||
|
|
||||||
# Pass 1: Select Top 10 from Tier 3 candidates
|
# Pass 1: Top 10 from Tier 3
|
||||||
tier3_candidates = pool[pool["platform_evidence_tier"] == 3]
|
tier3_candidates = pool[pool["platform_evidence_tier"] == 3]
|
||||||
top_10_indices = tier3_candidates.head(10).index.tolist()
|
top_10_indices = tier3_candidates.head(10).index.tolist()
|
||||||
assigned_indices.extend(top_10_indices)
|
assigned_indices.extend(top_10_indices)
|
||||||
|
|
||||||
# Pass 2: Select Positions 11 to 50 from remaining Tier >= 2 candidates
|
if len(assigned_indices) < min(10, len(pool)):
|
||||||
remaining_pool = pool.drop(index=assigned_indices)
|
shortfall_audit["top_10_shortfall"] = True
|
||||||
tier2_plus_candidates = remaining_pool[remaining_pool["platform_evidence_tier"] >= 2]
|
remaining_pool = pool.drop(index=assigned_indices)
|
||||||
top_50_fill_indices = tier2_plus_candidates.head(40).index.tolist()
|
shortfall = min(10 - len(assigned_indices), len(remaining_pool))
|
||||||
assigned_indices.extend(top_50_fill_indices)
|
backfill = remaining_pool[remaining_pool["platform_evidence_tier"] >= 2].head(shortfall)
|
||||||
|
assigned_indices.extend(backfill.index.tolist())
|
||||||
|
|
||||||
# Pass 3: Fill Positions 51+ with all remaining artists by score order
|
# Complete degraded fallback if Tier 2+ is also exhausted
|
||||||
|
if len(assigned_indices) < min(10, len(pool)):
|
||||||
|
remaining_pool = pool.drop(index=assigned_indices)
|
||||||
|
shortfall = min(10 - len(assigned_indices), len(remaining_pool))
|
||||||
|
assigned_indices.extend(remaining_pool.head(shortfall).index.tolist())
|
||||||
|
|
||||||
|
# Pass 2: Positions 11 to 50 from Tier >= 2
|
||||||
|
remaining_pool = pool.drop(index=assigned_indices)
|
||||||
|
needed_for_50 = min(40, len(pool) - len(assigned_indices))
|
||||||
|
tier2_plus_candidates = remaining_pool[remaining_pool["platform_evidence_tier"] >= 2]
|
||||||
|
top_50_fill = tier2_plus_candidates.head(needed_for_50).index.tolist()
|
||||||
|
assigned_indices.extend(top_50_fill)
|
||||||
|
|
||||||
|
if len(assigned_indices) < min(50, len(pool)):
|
||||||
|
shortfall_audit["top_50_shortfall"] = True
|
||||||
|
remaining_pool = pool.drop(index=assigned_indices)
|
||||||
|
shortfall = min(50 - len(assigned_indices), len(remaining_pool))
|
||||||
|
assigned_indices.extend(remaining_pool.head(shortfall).index.tolist())
|
||||||
|
|
||||||
|
# Pass 3: Positions 51+ with remaining artists by score order
|
||||||
final_remaining = pool.drop(index=assigned_indices)
|
final_remaining = pool.drop(index=assigned_indices)
|
||||||
assigned_indices.extend(final_remaining.index.tolist())
|
assigned_indices.extend(final_remaining.index.tolist())
|
||||||
|
|
||||||
# 3. Assign consecutive published ranks without touching raw scores
|
# 3. Assign contiguous published ranks
|
||||||
out["hiffi_xp_published_rank"] = pd.NA
|
out["hiffi_xp_published_rank"] = pd.NA
|
||||||
rank_series = pd.Series(
|
rank_series = pd.Series(
|
||||||
range(1, len(assigned_indices) + 1),
|
range(1, len(assigned_indices) + 1),
|
||||||
@@ -286,4 +322,6 @@ class HIFFIRankingEngine:
|
|||||||
out.loc[assigned_indices, "hiffi_xp_published_rank"] = rank_series
|
out.loc[assigned_indices, "hiffi_xp_published_rank"] = rank_series
|
||||||
out["hiffi_xp_published_rank"] = out["hiffi_xp_published_rank"].astype(int)
|
out["hiffi_xp_published_rank"] = out["hiffi_xp_published_rank"].astype(int)
|
||||||
|
|
||||||
|
if return_metadata:
|
||||||
|
return out, shortfall_audit
|
||||||
return out
|
return out
|
||||||
@@ -377,4 +377,82 @@ def test_publication_eligibility_gates(dataset):
|
|||||||
# 4. Check ranks are strictly unique and consecutive 1 to N
|
# 4. Check ranks are strictly unique and consecutive 1 to N
|
||||||
ranks = gated["hiffi_xp_published_rank"].sort_values().tolist()
|
ranks = gated["hiffi_xp_published_rank"].sort_values().tolist()
|
||||||
expected_ranks = list(range(1, len(gated) + 1))
|
expected_ranks = list(range(1, len(gated) + 1))
|
||||||
assert ranks == expected_ranks, "Published ranks contain gaps or duplicates."
|
assert ranks == expected_ranks, "Published ranks contain gaps or duplicates."
|
||||||
|
|
||||||
|
def test_publication_gate_blocks_unverified_identity():
|
||||||
|
"""Confirms that UNCERTAIN_NEEDS_REVIEW status does not count toward evidence tiers."""
|
||||||
|
engine = HIFFIRankingEngine()
|
||||||
|
test_df = pd.DataFrame([
|
||||||
|
{
|
||||||
|
"name": "Artist Pending Verification",
|
||||||
|
"production_hiffi_score": 95.0,
|
||||||
|
"spotify_monthly_listeners": 40_000_000,
|
||||||
|
"instagram_followers": 20_000_000,
|
||||||
|
"instagram_verification_status": "UNCERTAIN_NEEDS_REVIEW",
|
||||||
|
"hiffi_xp_score": 96.0,
|
||||||
|
"hiffi_xp_rank": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Artist Fully Verified",
|
||||||
|
"production_hiffi_score": 90.0,
|
||||||
|
"spotify_monthly_listeners": 35_000_000,
|
||||||
|
"instagram_followers": 15_000_000,
|
||||||
|
"instagram_verification_status": "AUTOMATED_VERIFIED",
|
||||||
|
"hiffi_xp_score": 91.0,
|
||||||
|
"hiffi_xp_rank": 2
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
gated = engine.apply_publication_eligibility_gates(test_df)
|
||||||
|
pending_row = gated[gated["name"] == "Artist Pending Verification"].iloc[0]
|
||||||
|
verified_row = gated[gated["name"] == "Artist Fully Verified"].iloc[0]
|
||||||
|
|
||||||
|
assert pending_row["platform_evidence_tier"] == 2
|
||||||
|
assert verified_row["platform_evidence_tier"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_publication_gate_missing_column_fails_closed():
|
||||||
|
"""Confirms that omitting the verification column fails closed (gives 0 IG credit)."""
|
||||||
|
engine = HIFFIRankingEngine()
|
||||||
|
test_df = pd.DataFrame([
|
||||||
|
{
|
||||||
|
"name": "Artist No Status Column",
|
||||||
|
"production_hiffi_score": 95.0,
|
||||||
|
"spotify_monthly_listeners": 40_000_000,
|
||||||
|
"instagram_followers": 20_000_000,
|
||||||
|
"hiffi_xp_score": 96.0,
|
||||||
|
"hiffi_xp_rank": 1
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
gated = engine.apply_publication_eligibility_gates(test_df)
|
||||||
|
assert gated.loc[0, "platform_evidence_tier"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_publication_gate_zero_multiplatform_cohort():
|
||||||
|
"""Confirms an entirely Tier 1 cohort produces consecutive ranks and flags an audited shortfall."""
|
||||||
|
engine = HIFFIRankingEngine()
|
||||||
|
|
||||||
|
records = [
|
||||||
|
{
|
||||||
|
"name": f"Solo Artist {i+1}",
|
||||||
|
"production_hiffi_score": 85.0 - i,
|
||||||
|
"spotify_monthly_listeners": np.nan,
|
||||||
|
"instagram_followers": np.nan,
|
||||||
|
"instagram_verification_status": "UNVERIFIED",
|
||||||
|
"hiffi_xp_score": 80.0 - i,
|
||||||
|
"hiffi_xp_rank": i + 1
|
||||||
|
}
|
||||||
|
for i in range(25)
|
||||||
|
]
|
||||||
|
|
||||||
|
tier1_df = pd.DataFrame(records)
|
||||||
|
gated, audit = engine.apply_publication_eligibility_gates(tier1_df, return_metadata=True)
|
||||||
|
|
||||||
|
# Must retain unique contiguous ordering 1 to 25
|
||||||
|
assert gated["hiffi_xp_published_rank"].tolist() == list(range(1, 26))
|
||||||
|
# All records remain Tier 1
|
||||||
|
assert (gated["platform_evidence_tier"] == 1).all()
|
||||||
|
# Shortfall must be explicitly reported
|
||||||
|
assert audit["top_10_shortfall"] is True
|
||||||
|
assert audit["top_50_shortfall"] is True
|
||||||
Reference in new issue
Block a user