feat: Opportunismus-Marker bei JA-Stimmen mit WP-Score < 3
Symmetrisch zur Heuchelei-Logik: bei JA-Fraktionen, deren eigener Wahlprogramm-Score < 3 ist, erscheint ein dezenter italic '!' mit Tooltip. 11 echte Cases gefunden auf dev (NRW + BB). app/marker.py: opportunismus_score() — neun neue Tests (test_marker.py jetzt 44 grün). Refs: ADR 0010, Phase 2.4 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a8f85bf3ee
commit
5823828fec
@ -146,8 +146,14 @@ app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||
templates = Jinja2Templates(directory=str(templates_dir))
|
||||
|
||||
# Detail-Marker im Jinja-Environment registrieren (siehe ADR 0010)
|
||||
from .marker import heuchelei_score, decisive_outcome, consistency_state # noqa: E402
|
||||
from .marker import ( # noqa: E402
|
||||
heuchelei_score,
|
||||
opportunismus_score,
|
||||
decisive_outcome,
|
||||
consistency_state,
|
||||
)
|
||||
templates.env.globals["heuchelei_score"] = heuchelei_score
|
||||
templates.env.globals["opportunismus_score"] = opportunismus_score
|
||||
templates.env.globals["decisive_outcome"] = decisive_outcome
|
||||
templates.env.globals["consistency_state"] = consistency_state
|
||||
|
||||
|
||||
@ -70,6 +70,36 @@ def decisive_outcome(plenum_votes: Optional[Iterable[dict]]) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def opportunismus_score(
|
||||
fraktion: str,
|
||||
fraktions_scores: Optional[Iterable[dict]],
|
||||
threshold: float = 3.0,
|
||||
) -> Optional[float]:
|
||||
"""Wahlprogramm-Score einer Fraktion, falls < threshold; sonst None.
|
||||
|
||||
Symmetrisch zu ``heuchelei_score``, aber für den Fall, dass eine
|
||||
Fraktion mit JA stimmt, obwohl der Antrag inhaltlich nicht zu
|
||||
ihrem Wahlprogramm passt (`wahlprogramm.score < 3`).
|
||||
|
||||
Wenn die Fraktion nicht gefunden wird, der Score fehlt oder
|
||||
≥ threshold ist, wird ``None`` zurückgegeben.
|
||||
"""
|
||||
if not fraktions_scores:
|
||||
return None
|
||||
for fs in fraktions_scores:
|
||||
if fs.get("fraktion") == fraktion:
|
||||
wp = fs.get("wahlprogramm") or {}
|
||||
score = wp.get("score")
|
||||
if score is None:
|
||||
return None
|
||||
try:
|
||||
score_f = float(score)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return score_f if score_f < threshold else None
|
||||
return None
|
||||
|
||||
|
||||
def consistency_state(
|
||||
verdict_title: Optional[str],
|
||||
plenum_votes: Optional[Iterable[dict]],
|
||||
|
||||
@ -301,7 +301,10 @@
|
||||
<div style="display:flex;flex-wrap:wrap;gap:12px;font-family:var(--font-mono);font-size:11px;">
|
||||
{% if v.fraktionen_ja %}
|
||||
<div><span style="color:#2da44e;font-weight:700;">Ja:</span>
|
||||
{% for f in v.fraktionen_ja %}<span style="display:inline-block;padding:1px 6px;background:color-mix(in srgb,#2da44e 15%,transparent);color:#1a7f37;border-radius:3px;margin-right:3px;">{{ f }}</span>{% endfor %}
|
||||
{% for f in v.fraktionen_ja %}
|
||||
{% set _opp_match = opportunismus_score(f, antrag.fraktions_scores) %}
|
||||
<span style="display:inline-block;padding:1px 6px;background:color-mix(in srgb,#2da44e 15%,transparent);color:#1a7f37;border-radius:3px;margin-right:3px;">{{ f }}{% if _opp_match is not none %}<span style="margin-left:4px;cursor:help;font-style:italic;color:#bf8700;" title="Diese Fraktion stimmte mit Ja, obwohl der Antrag nicht zum eigenen Wahlprogramm passt (WP-Score {{ '%.0f' | format(_opp_match) }}/10).">!</span>{% endif %}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if v.fraktionen_nein %}
|
||||
|
||||
@ -4,6 +4,7 @@ import pytest
|
||||
try:
|
||||
from app.marker import (
|
||||
heuchelei_score,
|
||||
opportunismus_score,
|
||||
decisive_outcome,
|
||||
consistency_state,
|
||||
DECISIVE_OUTCOMES,
|
||||
@ -75,6 +76,50 @@ class TestHeucheleiScore:
|
||||
assert heuchelei_score("CDU", fs) == 8.0
|
||||
|
||||
|
||||
# ─── opportunismus_score ────────────────────────────────────────────────────
|
||||
|
||||
class TestOpportunismusScore:
|
||||
"""opportunismus_score liefert WP-Score wenn Fraktion JA-stimmt aber WP<3."""
|
||||
|
||||
def test_score_low_returns_score(self):
|
||||
fs = [{"fraktion": "AfD", "wahlprogramm": {"score": 1.0}}]
|
||||
assert opportunismus_score("AfD", fs) == 1.0
|
||||
|
||||
def test_score_zero_returns_score(self):
|
||||
fs = [{"fraktion": "GRÜNE", "wahlprogramm": {"score": 0.0}}]
|
||||
assert opportunismus_score("GRÜNE", fs) == 0.0
|
||||
|
||||
def test_score_at_threshold_returns_none(self):
|
||||
# 3.0 ist genau Schwelle, sollte NICHT greifen (wir wollen < 3)
|
||||
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 3.0}}]
|
||||
assert opportunismus_score("CDU", fs) is None
|
||||
|
||||
def test_score_above_returns_none(self):
|
||||
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 5.0}}]
|
||||
assert opportunismus_score("CDU", fs) is None
|
||||
|
||||
def test_unknown_fraktion_returns_none(self):
|
||||
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 1.0}}]
|
||||
assert opportunismus_score("AfD", fs) is None
|
||||
|
||||
def test_empty_list_returns_none(self):
|
||||
assert opportunismus_score("CDU", []) is None
|
||||
|
||||
def test_none_input_returns_none(self):
|
||||
assert opportunismus_score("CDU", None) is None
|
||||
|
||||
def test_missing_score_returns_none(self):
|
||||
fs = [{"fraktion": "CDU", "wahlprogramm": {}}]
|
||||
assert opportunismus_score("CDU", fs) is None
|
||||
|
||||
def test_custom_threshold(self):
|
||||
fs = [{"fraktion": "CDU", "wahlprogramm": {"score": 4.0}}]
|
||||
# threshold=5 → 4 < 5 → opportunism greift
|
||||
assert opportunismus_score("CDU", fs, threshold=5.0) == 4.0
|
||||
# threshold=3 → 4 ≥ 3 → kein opportunism
|
||||
assert opportunismus_score("CDU", fs, threshold=3.0) is None
|
||||
|
||||
|
||||
# ─── decisive_outcome ───────────────────────────────────────────────────────
|
||||
|
||||
class TestDecisiveOutcome:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user