77from pathlib import Path
88import re
99from typing import Any
10+ from urllib .parse import urlparse
1011
1112import typer
1213from rapidfuzz .fuzz import token_set_ratio
7778}
7879BLOCKED_BOARD_TERMS = {
7980 "celebrity" ,
81+ "concert" ,
8082 "rod stewart" ,
8183 "ratbag" ,
8284 "opinion" ,
8890 "eurovision entry" ,
8991 "music museum" ,
9092 "father ted" ,
93+ "rapper" ,
94+ "red card controversy" ,
95+ "football chiefs" ,
96+ "training grounds" ,
97+ }
98+ SPONSORED_URL_MARKERS = {
99+ "/sponsored-content/" ,
100+ "/brandstudio/" ,
101+ "/paid-content/" ,
102+ "/partner-content/" ,
103+ }
104+ UNREADABLE_PRIMARY_HOSTS = {
105+ "politico.com" ,
106+ "www.politico.com" ,
107+ }
108+ TURKISH_READER_SIGNAL_TERMS = {
109+ "tariff" ,
110+ "tariffs" ,
111+ "trade" ,
112+ "sanctions" ,
113+ "inflation" ,
114+ "market" ,
115+ "markets" ,
116+ "energy" ,
117+ "oil" ,
118+ "gas" ,
119+ "defense" ,
120+ "defence" ,
121+ "security" ,
122+ "nato" ,
123+ "eu" ,
124+ "europe" ,
125+ "ukraine" ,
126+ "russia" ,
127+ "china" ,
128+ "trump" ,
129+ "ai" ,
130+ "artificial intelligence" ,
131+ "chip" ,
132+ "chips" ,
133+ "climate" ,
134+ "heatwave" ,
135+ "election" ,
136+ "migration" ,
137+ "central bank" ,
138+ "interest rates" ,
139+ "turkey" ,
140+ "türkiye" ,
141+ }
142+ LOW_PUBLIC_VALUE_ODDITY_TERMS = {
143+ "snake" ,
144+ "snakes" ,
145+ "cobra" ,
146+ "cobras" ,
147+ "venomous" ,
148+ "escaped" ,
149+ "rapper" ,
150+ "concert" ,
151+ "kanye" ,
152+ "football chiefs" ,
153+ "red card" ,
154+ "infantino" ,
91155}
92156
93157
@@ -164,6 +228,24 @@ def _source_name(item: Any) -> str:
164228 return item .draft_sources [0 ].name if item .draft_sources else "-"
165229
166230
231+ def _source_url (item : Any ) -> str :
232+ return str (item .draft_sources [0 ].url ) if item .draft_sources else ""
233+
234+
235+ def _source_host (item : Any ) -> str :
236+ host = urlparse (_source_url (item )).netloc .lower ()
237+ return host [4 :] if host .startswith ("www." ) else host
238+
239+
240+ def _is_sponsored_source (item : Any ) -> bool :
241+ url = _source_url (item ).lower ()
242+ return any (marker in url for marker in SPONSORED_URL_MARKERS )
243+
244+
245+ def _has_known_unreadable_primary_source (item : Any ) -> bool :
246+ return _source_host (item ) in UNREADABLE_PRIMARY_HOSTS
247+
248+
167249def _frontmatter_value (text : str , key : str ) -> str :
168250 match = re .search (rf"(?m)^{ re .escape (key )} :\s*[\"']?(.*?)[\"']?\s*$" , text )
169251 return match .group (1 ).strip () if match else ""
@@ -329,6 +411,32 @@ def _headline_has_term(headline: str, term: str) -> bool:
329411 return term in headline
330412
331413
414+ def _public_value_text (root : Path , item : Any ) -> str :
415+ normalized_store = JsonStore (root / "news_pipeline/data/normalized" , NormalizedArticle )
416+ article = normalized_store .load (item .normalized_id )
417+ parts = [
418+ item .draft_title or "" ,
419+ item .draft_description or "" ,
420+ " " .join (item .draft_tags or []),
421+ _source_name (item ),
422+ ]
423+ if article :
424+ parts .extend ([article .title or "" , article .summary or "" , article .content_snippet or "" , " " .join (article .tags or [])])
425+ return _normalized (" " .join (parts ))
426+
427+
428+ def _has_turkish_reader_signal (text : str ) -> bool :
429+ return any (_headline_has_term (text , term ) for term in TURKISH_READER_SIGNAL_TERMS )
430+
431+
432+ def _has_low_public_value_oddity (text : str ) -> bool :
433+ return any (_headline_has_term (text , term ) for term in LOW_PUBLIC_VALUE_ODDITY_TERMS )
434+
435+
436+ def _has_board_hard_veto (reasons : list [str ]) -> bool :
437+ return any (reason .startswith ("public_value_penalty:" ) for reason in reasons )
438+
439+
332440def _board_score (
333441 root : Path ,
334442 item : Any ,
@@ -338,6 +446,7 @@ def _board_score(
338446 recent_posts : list [dict [str , str ]] | None = None ,
339447) -> tuple [float , list [str ]]:
340448 headline = _normalized (_headline_text (root , item ))
449+ public_value_text = _public_value_text (root , item )
341450 category = item .draft_category or ""
342451 source = _normalized (_source_name (item ))
343452 score = float (item .editorial_priority )
@@ -371,6 +480,7 @@ def _board_score(
371480 repeated = "+" .join (sorted (repeat_counts ))
372481 reasons .append (f"recency_penalty:company_repeat:{ repeated } :{ strongest_repeat } " )
373482 family_hits = _item_topic_family_hits (root , item )
483+ has_recent_family_repeat = False
374484 if family_hits :
375485 recent_family_counts = recent_live_topic_family_counts (
376486 root / "src/content/equinoxHaber" ,
@@ -382,6 +492,7 @@ def _board_score(
382492 if recent_family_counts [family ] >= RECENT_TOPIC_FAMILY_PENALTY_THRESHOLD
383493 }
384494 if repeat_counts :
495+ has_recent_family_repeat = True
385496 strongest_repeat = max (repeat_counts .values ())
386497 family_penalty = min (RECENT_TOPIC_FAMILY_PENALTY_MAX , strongest_repeat * RECENT_TOPIC_FAMILY_PENALTY_PER_ITEM )
387498 score -= family_penalty
@@ -402,6 +513,15 @@ def _board_score(
402513 score += 0.035
403514 reasons .append (f"signal_boost:{ term } " )
404515 break
516+ if _has_turkish_reader_signal (public_value_text ) and not localized_crime and not incidental_security and not has_recent_family_repeat :
517+ score += 0.045
518+ reasons .append ("signal_boost:turkish_reader_interest" )
519+ if _has_low_public_value_oddity (public_value_text ):
520+ oddity_penalty = 0.18
521+ if _has_turkish_reader_signal (public_value_text ):
522+ oddity_penalty = 0.10
523+ score -= oddity_penalty
524+ reasons .append ("public_value_penalty:oddity_or_entertainment" )
405525 if science_pressure and category == "Bilim" :
406526 score -= SCIENCE_RECENT_PENALTY
407527 reasons .append ("recency_penalty:science_saturation" )
@@ -424,6 +544,10 @@ def _passes_basic_board_filter(root: Path, item: Any, max_source_age_hours: int,
424544 live_urls = live_urls or set ()
425545 if any (str (source .url ) in live_urls for source in item .draft_sources ):
426546 return False , "source already published"
547+ if _is_sponsored_source (item ):
548+ return False , "sponsored or paid content"
549+ if _has_known_unreadable_primary_source (item ):
550+ return False , "known unreadable primary source"
427551 fresh , stale_reason = _source_is_fresh (root , item , max_source_age_hours )
428552 if not fresh :
429553 return False , stale_reason
@@ -479,6 +603,7 @@ def _select_headline_board(root: Path, items: list[Any], limit: int, max_source_
479603 eligible .append ((item , board_score , reasons ))
480604
481605 eligible .sort (key = lambda row : row [1 ], reverse = True )
606+ score_map = {item .queue_id : (score , reasons ) for item , score , reasons in eligible }
482607 selected : list [Any ] = []
483608 selected_ids : set [str ] = set ()
484609 selected_headlines : list [str ] = []
@@ -489,6 +614,9 @@ def _select_headline_board(root: Path, items: list[Any], limit: int, max_source_
489614 def add (item : Any ) -> bool :
490615 if item .queue_id in selected_ids :
491616 return False
617+ _ , board_reasons = score_map .get (item .queue_id , (round (float (item .editorial_priority ), 3 ), []))
618+ if _has_board_hard_veto (board_reasons ):
619+ return False
492620 headline = _normalized (_headline_text (root , item ))
493621 if any (token_set_ratio (headline , existing ) >= 88 for existing in selected_headlines ):
494622 return False
@@ -531,7 +659,6 @@ def add(item: Any) -> bool:
531659 break
532660 add (item )
533661
534- score_map = {item .queue_id : (score , reasons ) for item , score , reasons in eligible }
535662 selected .sort (key = lambda item : score_map .get (item .queue_id , (round (float (item .editorial_priority ), 3 ), []))[0 ], reverse = True )
536663 diagnostics = {
537664 "eligibleCount" : len (eligible ),
0 commit comments