v1.14.0: refactor OV-filter naar driestapsmodel — één regel i.p.v. drie uitzonderingstakken
- _filter_passes: 'zwak-alleen in samenvatting' en 'sterk+zwak uitsluitend in samenvatting' reduceren tot één regel: zonder enig titel-signaal (familie-hit, whitelist-term of storingszin) mag de score geen zwak woord bevatten. Equivalentie bewezen over de volledige beslissing-ruimte (420 configuraties). - Nieuwe dienststoring-context 'vervoerswoord rijdt/rijden niet' (gedeeld patroon): sterk titel-signaal (+2) en familie (2) — 'sprinters rijden niet tussen Utrecht en Amsterdam', 'metro rijdt niet tussen Rotterdam en Schiedam' passeren nu; bus/tram/metro/sprinter zelf blijven zwak/medium. - Overlappende regexes opgeruimd: busbaan/bushokje uit de halte-familie. - Blacklist 29 -> 2 woorden: restant (stationsstraat, stationair, stationcar, stationeren ...) was inert sinds de woordgrens-familie; vastgelegd in tests. - validate_filter.py: 56 -> 90 regressie-cases + invariant 4 (whitelist-term in familie, blacklist-term door familie gematcht). - _filter_summary/beleidsdocumentatie geactualiseerd.
This commit is contained in:
parent
1346cbb549
commit
91d0c3d7fe
3 changed files with 215 additions and 141 deletions
297
buzzrss/bot.py
297
buzzrss/bot.py
|
|
@ -56,10 +56,12 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
|
|||
# Per familie wordt 1× geteld per ARTIKEL (titel én samenvatting samen; herhaling
|
||||
# of het woord in beide velden = één signaal — 'busstation' 60× weegt 1×, besluit
|
||||
# 2026-09-11). Eén term die meerdere families matcht telt óók maar 1× (hoogste
|
||||
# gewicht wint): 'busbaan' zit in de bus-samenstelling- én de halte/busbaan-familie,
|
||||
# 'ov-chipkaart' bevat 'chipkaart', 'openbaar vervoer' bevat 'vervoer' — geen
|
||||
# dubbeltelling (besluit 2026-09-13). Drempel (3): doorlaten als de som van de
|
||||
# unieke familie-gewichten >= MATCH_FILTER_THRESHOLD (titel weegt niet extra).
|
||||
# gewicht wint): 'ov-chipkaart' bevat 'chipkaart', 'openbaar vervoer' bevat
|
||||
# 'vervoer', 'treinstoring' matcht de zwakke \w*trein\w*-familie mee — geen
|
||||
# dubbeltelling (besluit 2026-09-13; 'busbaan' stond in twee families en is
|
||||
# uit de halte-familie verwijderd, refactor 2026-09-13). Drempel (3): doorlaten
|
||||
# als de som van de unieke familie-gewichten >= MATCH_FILTER_THRESHOLD
|
||||
# (titel weegt niet extra).
|
||||
# MAX-GEWICHT 2 — besluit 2026-09-12: een enkele FAMILIE-hit kan nooit solo
|
||||
# doorlaten (2 < 3); écht OV-nieuws combineert in de praktijk altijd met een
|
||||
# tweede signaal (gevalideerd op 205 productie-artikelen; zie beleidsblok).
|
||||
|
|
@ -105,38 +107,37 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
|
|||
# Doel: precisie boven recall. Liever een OV-bericht te veel gefilterd dan
|
||||
# een niet-OV-bericht te veel doorgelaten (gebruikersbesluit, 2026-09-10).
|
||||
#
|
||||
# Lagen, elk met een eigen regel (MAX-2-stelsel, besluit 2026-09-12):
|
||||
# 1. WHITELIST -> termen die in élke context OV betekenen (merken:
|
||||
# Beslissing, in drie stappen (refactor 2026-09-13; logica equivalent met de
|
||||
# voorheen gelaagde takken — MAX-2-stelsel, besluit 2026-09-12):
|
||||
# 1. STERK SIGNAAL (whitelist-term óf dienststoring-zin in de TITEL) -> DOORLATEN.
|
||||
# Whitelist = termen die in élke context OV betekenen (merken:
|
||||
# NS/ProRail/Arriva; onmiskenbaar jargon: spoorstoring, perron, bushalte,
|
||||
# conducteur, ovpay; multi-woord-frasen: nederlandse spoorwegen, openbaar
|
||||
# vervoer, ice-berlijn ...). Een whitelist-hit in de TITEL telt +2 (geen
|
||||
# auto-pass meer). Omdat elke whitelist-term óók in een familie zit, is dat
|
||||
# effectief het oude titel-anker voor onmiskenbare termen — 'Reactie
|
||||
# Nederlandse Spoorwegen' blijft daardoor door. REGEL: twijfel je of de term
|
||||
# ook buiten het OV kan voorkomen -> NIET hier, maar zwak (eigen familie).
|
||||
# Een whitelist-term ALLEEN in de SAMENVATTING blijft inert: een bijzin als
|
||||
# 'de zakelijke bezoeker is ook nodig voor het openbaar vervoer' in een
|
||||
# economiebericht is geen OV-nieuws (besluit 2026-09-12); zo'n hit scoort
|
||||
# alleen via de overeenkomstige familie.
|
||||
# 2. GEWOGEN FAMILIES -> score = som van unieke OV-families over het hele
|
||||
# artikel (titel + samenvatting; per familie 1× per artikel — herhaling of
|
||||
# beide velden tellen niet dubbel, besluit 2026-09-11) >= drempel (3).
|
||||
# Overlappende treffers van verschillende families (één term op één plek)
|
||||
# tellen samen als 1× met het hoogste gewicht — een term mag nooit dubbel
|
||||
# wegen ('busbaan' in de bus-samenstelling- én de halte/busbaan-familie,
|
||||
# besluit 2026-09-13).
|
||||
# vervoer, ice-berlijn ...). Een whitelist-hit in de titel telt +2,
|
||||
# bovenop het familie-gewicht van dezelfde term (elke whitelist-term zit
|
||||
# óók in een familie) — samen altijd >= drempel, dus eenduidige OV-titels
|
||||
# ('Reactie Nederlandse Spoorwegen', 'Werkzaamheden aan perron') passeren
|
||||
# alleen. REGEL: twijfel je of de term ook buiten het OV kan voorkomen ->
|
||||
# NIET hier, maar zwak (eigen familie). Een whitelist-term ALLEEN in de
|
||||
# SAMENVATTING blijft inert: een bijzin als 'de zakelijke bezoeker is ook
|
||||
# nodig voor het openbaar vervoer' in een economiebericht is geen
|
||||
# OV-nieuws (besluit 2026-09-12); zo'n hit scoort alleen via de familie.
|
||||
# 2. CONTEXT: som van unieke OV-signalen over titel + samenvatting
|
||||
# >= drempel (3). Per familie 1× per artikel (herhaling of beide velden
|
||||
# tellen niet dubbel, besluit 2026-09-11); overlappende familie-treffers
|
||||
# van dezelfde term wegen 1× met het hoogste gewicht (span-merge in
|
||||
# _fam_hits, besluit 2026-09-13 — 'ov-chipkaart' bevat 'chipkaart' maar
|
||||
# weegt als één term 2, geen 3).
|
||||
# - sterk (2): onmiskenbaar OV-jargon/merken/samenvoegingen (spoorlijn,
|
||||
# bushalte, busverkeer, intercity, veerboot, ertms,
|
||||
# rangeer..., ice-frasen, ...). Eén hit (titel óf samenvatting)
|
||||
# is onvoldoende: moet combineren (2 + 1 = 3). De vroegere
|
||||
# 'één sterk woord volstaat'-regel is vervallen — 'hermes'
|
||||
# als achternaam passeerde zo op één vervoerder-hit
|
||||
# (Margraten, 2026-09-12).
|
||||
# EXTRA (besluit 2026-09-12): een sterk/medium + zwak-
|
||||
# combinatie UITSLUITEND in de samenvatting (geen enkele hit
|
||||
# in de titel én geen whitelist-term in de titel) is geen
|
||||
# OV-bewijs — 'openbaar vervoer' + 'zakelijke reizigers' in
|
||||
# een economiebericht telt anders op tot de drempel.
|
||||
# rangeer..., ice-frasen, ...) én het dienststoring-
|
||||
# contextpatroon 'vervoerswoord + rijdt/rijden (niet) +
|
||||
# niet' ('sprinters rijden niet', 'metro rijdt niet').
|
||||
# Eén hit (titel óf samenvatting) is onvoldoende: moet
|
||||
# combineren (2 + 1 = 3). De vroegere 'één sterk woord
|
||||
# volstaat'-regel is vervallen — 'hermes' als achternaam
|
||||
# passeerde zo op één vervoerder-hit (Margraten,
|
||||
# 2026-09-12).
|
||||
# - zwak (1): DUBBELZINNIGHEID. Woorden die (ook) buiten het OV
|
||||
# voorkomen. REGEL: elk dubbelzinnig woord krijgt een EIGEN
|
||||
# familie (niet samengevoegd), zodat twee verschillende
|
||||
|
|
@ -145,18 +146,27 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
|
|||
# trein, bus, station(s), sprinter, reiziger/passagier,
|
||||
# vervoerder, inchecken/uitchecken, sein, overweg, breng,
|
||||
# kaartje, opwaarderen, rails, dienstregeling, chipkaart,
|
||||
# ontsporen, pendelbus/shuttlebus (evenement-/hotel-/
|
||||
# parkeerpendel; synoniemen -> bewust één familie),
|
||||
# busongeluk/busvervoer (touringcar/schoolbus), staking,
|
||||
# vervoer, vervoersbedrijf, vervoerregio, ritten,
|
||||
# bovenleiding (industrie), abonnement, hermes, spoor.
|
||||
# EXTRA REGEL voor zwak-alleen: staat er uitsluitend zwak
|
||||
# bewijs in de SAMENVATTING (geen enkele zwakke hit in de
|
||||
# titel), dan niet doorlaten. Twee/meer zwakke woorden in
|
||||
# de samenvatting zijn anders samen al 3 punten
|
||||
# ("station" + "pendelbus" + "bus" in een golfverslag).
|
||||
# 3. BLACKLIST -> gematchte woorden zonder OV-waarde (stationsstraat,
|
||||
# stationair, stationcar, ...); blokkeren de familie niet.
|
||||
# reisproduct, vervoer(s)bewijs, ontsporen,
|
||||
# pendelbus/shuttlebus (evenement-/hotel-/parkeerpendel;
|
||||
# synoniemen -> bewust één familie), busongeluk/busvervoer
|
||||
# (touringcar/schoolbus), staking, vervoer, vervoersbedrijf,
|
||||
# vervoerregio, ritten, bovenleiding (industrie),
|
||||
# abonnement, hermes, spoor.
|
||||
# 3. TITEL-ANKER: staat er geen enkel OV-signaal in de titel (geen
|
||||
# familie-hit, geen whitelist-term), dan is de score uitsluitend
|
||||
# samenvatting-bewijs en mag die geen zwak woord bevatten. Twee/meer
|
||||
# zwakke woorden in de samenvatting ('station' + 'pendelbus' + 'bus' in
|
||||
# een golfverslag) en sterk+zwak uitsluitend in de samenvatting
|
||||
# ('openbaar vervoer' + 'zakelijke reizigers' in een economiebericht,
|
||||
# beide besluiten 2026-09-12) zijn geen OV-bewijs. Alleen twee sterke
|
||||
# termen in de samenvatting blijven bewust door (delftopzondag
|
||||
# 'tram 1, halte Buitenhofdreef', besluit 2026-09-13).
|
||||
# BLACKLIST -> gematchte woorden zonder OV-waarde, restant van
|
||||
# straatnamen rond spoorlijnen die de zwakke \b\w*trein\w*\b-familie zou
|
||||
# matchen ('treinstraat'/'treinweg'); blokkeren de familie niet. De
|
||||
# oudere stations*-varianten zijn sinds de woordgrens-familie
|
||||
# \bstations?\b inert en verwijderd (2026-09-13) — gedrag vastgelegd in
|
||||
# validate_filter.py.
|
||||
#
|
||||
# BEOORDELEN VAN EEN NIEUW WOORD (incl. COMPLETEHEIDS-AUDIT, 2026-09-12):
|
||||
# a) Kan het ook buiten het OV voorkomen? ja -> zwak (1), eigen familie.
|
||||
|
|
@ -174,6 +184,17 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
|
|||
# spoorvervoerder, treinvertraging/uitval, ertms, teso, rangeer, ice-frasen,
|
||||
# maaslijn, ns international, deutsche bahn, european sleeper, italo,
|
||||
# nightjet (sterk) en 11 zwakke woorden op.
|
||||
# Dienststoring-contextpatroon: vervoerswoord + 'rijdt/rijden (niet)' + 'niet'
|
||||
# ('sprinters rijden niet', 'metro rijdt niet tussen Rotterdam en Schiedam').
|
||||
# Gedeeld door de familie én het titel-anker (_disruption_hit): in de TITEL is de
|
||||
# storingszin een sterk OV-signaal (net als een whitelist-term), in de samenvatting
|
||||
# weegt hij als familie (2). 'rijden' an sich is géén OV-signaal. 'schoolbus'
|
||||
# matcht niet ('bus' niet aan het woordbegin), refactor 2026-09-13.
|
||||
_DISRUPTION_RE_SRC = (
|
||||
r"\b(?:sprinters?|stoptrein(?:en)?|intercity'?s?|trein(?:en)?|trams?|bussen|bus|"
|
||||
r"metro'?s?|veerdienst(?:en)?)\s+(?:rijdt|rijden|reed|reden)\s+niet\b"
|
||||
)
|
||||
|
||||
# =====================================================================
|
||||
MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
|
||||
# sterk (2) — onmiskenbaar OV, maar één hit is onvoldoende (max-2-stelsel,
|
||||
|
|
@ -182,7 +203,7 @@ MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
|
|||
(r"\b(?:spoorweg|spoorba(?:an|nen)|spoorlijn|spoorverkeer|spoorwegovergang|spoorstoring|perron|seinstoring|wisselstoring|spoorstaking|spoorsector|spoorbeheerder|spoornet|spoorvervoerder|ertms)\w*\b", 2), # spoorinfra; let op de klinkerwisseling: 'ontsporing' (open lettergreep, één o) vs 'ontspoord'/'ontspoorde' (dicht, dubbel o) — vandaar beide stammen. Géén 'spoorbrug': die is 2 maar vaak louter locatie (besluit 2026-09-12). Uitbreiding 2026-09-12 (completheids-audit): spoorsector, spoorbeheerder, spoornet, spoorvervoerder, ertms
|
||||
(r"\b(?:treinstation|treinstations|metrostation|metrostations|busstation|busstations)\b", 2), # station-samenstellingen: onmiskenbaar OV, maar één station-woord náást een ander onderwerp ('pand naast busstation', besluit 2026-09-11) is geen OV-bericht; in de samenvatting moet een tweede OV-woord bijkomen
|
||||
(r"(?<![\w-])\bstations?\b", 1), # los 'station(s)' is ZWAK: ook tv-/radio-/regionale omroepstations ('de lokale stations'), moet combineren met een andere OV-familie
|
||||
(r"\b(?:bus|tram|metro)-?(?:halte|haltes|lijn|lijnen|verkeer|net|staking|ba(?:an|nen)|hok(?:je|jes)|chauffeur)\w*\b", 2), # voertuig-samenstelling (bushalte/buslijn/bushokje/busbanen/buschauffeur/…); géén 'station': *station-samenstellingen zitten in de station-familie, anders telde 'busstation' dubbel mee — fix 2026-09-11. Géén 'ongeluk'/'vervoer': die zijn bewust zwak (busongeluk = ook touringcar, busvervoer = ook bedrijfsvervoer, audit 2026-09-12). 'busbaan' staat óók in fam 13: overlappende treffers tellen 1× met het hoogste gewicht (besluit 2026-09-13)
|
||||
(r"\b(?:bus|tram|metro)-?(?:halte|haltes|lijn|lijnen|verkeer|net|staking|ba(?:an|nen)|hok(?:je|jes)|chauffeur)\w*\b", 2), # voertuig-samenstelling (bushalte/buslijn/bushokje/busbanen/buschauffeur/…); géén 'station': *station-samenstellingen zitten in de station-familie, anders telde 'busstation' dubbel mee — fix 2026-09-11. Géén 'ongeluk'/'vervoer': die zijn bewust zwak (busongeluk = ook touringcar, busvervoer = ook bedrijfsvervoer, audit 2026-09-12). 'busbaan'/'bushokje' stonden óók in de halte-familie — verwijderd bij de refactor 2026-09-13 (geen dubbelwoorden in twee families)
|
||||
(r"\b(?:treinstoring|treinverkeer|treinstaking|treinongeval|treinbotsing|treinongeluk|treinvertraging|treinhinder|treinuitval|treinverstoring)\w*\b", 2), # trein-jargon-samenstellingen: onmiskenbaar OV; uitgebreid met vertraging/hinder/uitval/verstoring (audit 2026-09-12)
|
||||
(r"\b\w*trein\w*\b", 1), # losse 'trein' en algemene samenstellingen (stoptrein, goederentrein, treinverkeer al in bovenstaande familie) — ZWAK: 'de trein naar mijn nieuwe huis' is geen OV-jargon, moet combineren met een andere familie
|
||||
(r"\b(?:intercity|streekvervoer|stadsvervoer|veerdienst|veerboot|veerboten|veerpont|voetveer|veerverbinding|buurtbus|belbus|maaslijn|nedersaksenlijn|kamperlijn)\w*\b", 2), # treintypes, vervoerssectoren, veren en lijnnamen ('sprinter' staat in de zwakke groep: atleet-botsing); maaslijn/nedersaksenlijn/kamperlijn via audit 2026-09-12
|
||||
|
|
@ -192,9 +213,10 @@ MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
|
|||
# onmiskenbaar OV maar iets algemener (2, moet ook combineren)
|
||||
(r"\btrams?\b", 2),
|
||||
(r"\bmetro(?:'?s)?\b", 2),
|
||||
(r"\b(?:openbaar vervoer|lightrail|halte|bushokje|busbaan)\w*\b", 2), # 'busbaan' ook in fam 4, 'openbaar vervoer' bevat 'vervoer' (fam 41): één term = één signaal (besluit 2026-09-13)
|
||||
(r"\b(?:openbaar vervoer|lightrail|halte)\w*\b", 2), # frasen en halte-hits; 'bushokje'/'busbaan' matchen al de bus-samenstelling-familie — bewust niet dubbel in twee families (refactor 2026-09-13)
|
||||
(r"\b(?:ov-chipkaart|ovpay|ov-pay|studentenreisproduct)\w*\b", 2),
|
||||
(r"\b(?:ov-staking|conducteursstaking)\w*\b", 2),
|
||||
(_DISRUPTION_RE_SRC, 2), # dienststoring-context ('sprinters rijden niet', 'metro rijdt niet...'); zie _DISRUPTION_RE_SRC
|
||||
(r"\bconduct(?:eur|eurs)\b", 2), # trein-/buspersoneel: onmiskenbaar OV-jargon
|
||||
(r"\b(?:pont|ponten)\b", 2), # waterbus/veerdienst
|
||||
(r"\bspoorbrug(?:gen)?\b", 2), # spoorinfra, maar vaak locatie-referentie ('bij de spoorbrug', Stuifmail 2026-09-12); moet combineren — zelfde regel als station-samenstellingen
|
||||
|
|
@ -240,21 +262,16 @@ MATCH_FILTER_THRESHOLD = 3
|
|||
# Woorden die een familie wél matcht maar die geen OV-waarde hebben. Een hit dat
|
||||
# hierin staat telt niet mee voor de score (en blokkeert de familie niet). Vorm:
|
||||
# exact woord (lowercase), zoals door de familie-regex wordt gematcht.
|
||||
# Refactor 2026-09-13: alleen woorden die een HUIDIGE familie nog matchen blijven
|
||||
# staan ('treinstraat'/'treinweg' via de zwakke \b\w*trein\w*\b-familie). De
|
||||
# stations*-varianten (stationsstraat, stationair, stationeren, stationcar,
|
||||
# stationwagen, stationsplein, ...) waren inert sinds de woordgrens-familie
|
||||
# \bstations?\b en zijn verwijderd — dat gedrag is in validate_filter.py
|
||||
# vastgelegd.
|
||||
MATCH_FILTER_BLACKLIST: frozenset[str] = frozenset({
|
||||
# Straatnamen rond stations (genitief-s-vorm van "\bstation\w*\b"): een straat
|
||||
# die toevallig bij een station ligt maakt een bericht nog geen OV-bericht.
|
||||
"stationsstraat", "stationsweg", "stationslaan", "stationsplein",
|
||||
"stationssingel", "stationskade", "stationsdreef", "stationshof",
|
||||
"stationspad", "stationsboulevard", "stationsgracht", "stationsdijk",
|
||||
"stationssteeg", "stationswal", "stationsbuurt", "stationswijk", "stationspark",
|
||||
# Idem voor "\btrein\w*\b".
|
||||
# Straatnamen rond spoorlijnen die de \b\w*trein\w*\b-familie zou matchen:
|
||||
# een straat die toevallig 'Trein' heet maakt een bericht geen OV-bericht.
|
||||
"treinstraat", "treinweg",
|
||||
# Idem voor "\bstation\w*\b": werkwoords-/bijvoeglijke vormen zonder OV-waarde
|
||||
# ("motor stationair laten draaien", "auto's hier stationeren") en
|
||||
# samenstellingen met een station in de naam ("stationcar", "stationwagen").
|
||||
"stationair", "stationeel", "stationeren", "stationeert", "stationeerde",
|
||||
"stationeerden", "stationerend", "stationcar", "stationwagen",
|
||||
"stationnement", "stationering",
|
||||
})
|
||||
|
||||
# MATCH_FILTER_WHITELIST: termen die een bericht per definitie OV maken. Sinds
|
||||
|
|
@ -308,6 +325,9 @@ _WHITELIST_RE = re.compile(
|
|||
)
|
||||
_WHITELIST_PHRASES: tuple[str, ...] = tuple(w for w in MATCH_FILTER_WHITELIST if " " in w)
|
||||
|
||||
# Dienststoring-context als sterk TITEL-signaal (zie _DISRUPTION_RE_SRC).
|
||||
_DISRUPTION_RE = re.compile(_DISRUPTION_RE_SRC, re.IGNORECASE)
|
||||
|
||||
|
||||
class Config(BaseProxyConfig):
|
||||
def do_update(self, helper: ConfigUpdateHelper) -> None:
|
||||
|
|
@ -447,6 +467,14 @@ class RSSBot(Plugin):
|
|||
low = text.lower()
|
||||
return any(ph in low for ph in _WHITELIST_PHRASES)
|
||||
|
||||
@staticmethod
|
||||
def _disruption_hit(text: str) -> bool:
|
||||
"""True als een dienststoring-zin ('sprinters rijden niet', 'metro rijdt
|
||||
niet tussen Rotterdam en Schiedam') in text staat. In de TITEL telt zo'n
|
||||
zin als sterk OV-signaal (+2, net als een whitelist-term); als familie
|
||||
(2) scoort hij overal — zie _DISRUPTION_RE_SRC."""
|
||||
return _DISRUPTION_RE.search(text) is not None
|
||||
|
||||
@staticmethod
|
||||
def _fam_hits(text: str) -> list[tuple[int, int]]:
|
||||
"""(familie-index, gewicht) per OV-signaal in text (1× per familie).
|
||||
|
|
@ -458,10 +486,10 @@ class RSSBot(Plugin):
|
|||
Eén term telt maximaal één signaal (besluit 2026-09-13): overlappende
|
||||
treffers van verschillende families op dezelfde plek worden samengevoegd
|
||||
met het hoogste gewicht. Voorheen telde één woord dubbel als het in
|
||||
meerdere families stond — 'busbaan' zit in de bus-samenstelling-familie
|
||||
én in de halte/busbaan-familie (2+2=4), 'ov-chipkaart' bevat 'chipkaart'
|
||||
(2+1=3), 'openbaar vervoer' bevat 'vervoer' (2+1=3). Max-2-stelsel:
|
||||
één term kan nooit solo doorlaten."""
|
||||
meerdere families stond — 'ov-chipkaart' bevat 'chipkaart' (2+1=3),
|
||||
'openbaar vervoer' bevat 'vervoer' (2+1=3), 'treinstoring' matcht ook
|
||||
de zwakke \\w*trein\\w*-familie (2+1=3). Max-2-stelsel: één term kan
|
||||
nooit solo doorlaten."""
|
||||
spans: list[tuple[int, int, int, int]] = [] # (start, end, familie-idx, gewicht)
|
||||
for idx, (family, weight) in enumerate(_compiled_match_families):
|
||||
for m in family.finditer(text):
|
||||
|
|
@ -484,68 +512,66 @@ class RSSBot(Plugin):
|
|||
|
||||
@staticmethod
|
||||
def _filter_passes(title: str, summary: str) -> bool:
|
||||
"""Gewogen familiescore (max-2-stelsel, besluit 2026-09-12): elke OV-familie
|
||||
telt 1× per artikel (titel + samenvatting samen; herhaling of beide velden =
|
||||
één signaal). Overlappende treffers van verschillende families tellen samen
|
||||
als 1× met het hoogste gewicht — 'busbaan' (bus-samenstelling- én
|
||||
halte/busbaan-familie) levert één signaal van 2, geen 4 (besluit 2026-09-13).
|
||||
Doorlaten bij som >= drempel (3). Geen enkele familie weegt
|
||||
meer dan 2, dus een enkele familie-hit kan nooit solo doorlaten — 'hermes'
|
||||
als achternaam passeerde vroeger op één vervoerder-hit van 3 (Margraten,
|
||||
2026-09-12); écht OV-nieuws combineert in de praktijk altijd met een tweede
|
||||
signaal (gevalideerd op 205 productie-artikelen).
|
||||
"""Driestaps-beslissing, precision-first (refactor 2026-09-13; beslissing
|
||||
equivalent met v1.13.0, maar de drie historische uitzonderings-takken
|
||||
reduceren tot één regel — zie stap 3):
|
||||
|
||||
Whitelist-termen (onmiskenbaar OV in élke context) tellen in de TITEL +2 —
|
||||
géén auto-pass meer; omdat elke whitelist-term óók in een familie zit, is
|
||||
dat effectief het behouden titel-anker ('Reactie Nederlandse Spoorwegen',
|
||||
'Werkzaamheden aan perron', 'Buurtbus rijdt niet meer' blijven door). Een
|
||||
whitelist-term ALLEEN in de samenvatting is geen OV-bewijs (besluit
|
||||
2026-09-12: 'openbaar vervoer' als bijzin in een economiebericht) en scoort
|
||||
alleen via de overeenkomstige familie.
|
||||
1. STERK SIGNAAL: een whitelist-term óf een dienststoring-zin
|
||||
('sprinters rijden niet') in de titel is voldoende. Elke
|
||||
whitelist-term staat óók in een familie, dus zo'n titel telt +2 én
|
||||
het familie-gewicht — samen altijd >= drempel ('NS meldt storing',
|
||||
'Werkzaamheden aan perron', 'Buurtbus rijdt niet meer'). In de
|
||||
samenvatting ALLEEN is zo'n term géén bewijs ('openbaar vervoer'
|
||||
als bijzin in een economiebericht, besluit 2026-09-12) en scoort
|
||||
die alleen via de overeenkomstige familie.
|
||||
|
||||
Zwakke woorden (losse 'bus', 'breng', 'overweg', 'overwegen', 'sein',
|
||||
'opwaarderen', 'kaartje', 'rails', 'pendelbus', 'spoor', 'staking',
|
||||
'vervoer', 'busongeluk', ...) moeten combineren met een ander OV-woord én
|
||||
minstens één zwak woord moet in de titel staan (of een whitelist-term in de
|
||||
titel); dezelfde zwakke familie in titel én samenvatting is géén combinatie.
|
||||
Een sterk (2) + zwak-combinatie UITSLUITEND in de samenvatting (geen
|
||||
familie-hit én geen whitelist-term in de titel) is geen OV-bewijs
|
||||
('openbaar vervoer' + 'zakelijke reizigers' in een economiebericht, besluit
|
||||
2026-09-12). Weert oa 'VW-bus' (bus matcht niet ná een koppelteken),
|
||||
'schoolbus', 'bussen rijden om' in een woningbrand-lead, het werkwoord
|
||||
'brengen', het idioom 'goed met elkaar overweg kunnen', 'een wijk
|
||||
opwaarderen', brandweerjargon 'sein brandmeester', figuratief 'weer op de
|
||||
rails', een golfverslag waarin de samenvatting 'station' + 'pendelbus' +
|
||||
'bus' noemt, en het Margraten-bericht met 'Ton Hermes' als achternaam.
|
||||
Bewust doorgelaten: krant 'De Metro'/café 'De Tram' (2, combineert) en
|
||||
atleet-'sprinter' in combinatie met ander OV-woord."""
|
||||
2. CONTEXT: de som van unieke OV-signalen over titel + samenvatting
|
||||
>= drempel (3). Eén term telt één signaal per artikel ('busstation'
|
||||
60× of in beide velden = één keer); overlappende familie-treffers
|
||||
van dezelfde term wegen 1× met het hoogste gewicht (span-merge in
|
||||
_fam_hits, besluit 2026-09-13 — 'ov-chipkaart' bevat 'chipkaart'
|
||||
maar weegt als één term 2, geen 3). Geen
|
||||
familie weegt meer dan 2 (max-2-stelsel, besluit 2026-09-12): een
|
||||
enkel sterk woord kan nooit solo doorlaten ('hermes' als
|
||||
achternaam passeerde vroeger op één vervoerder-hit van 3 —
|
||||
Margraten; écht OV-nieuws combineert in de praktijk altijd met een
|
||||
tweede signaal, gevalideerd op 205 productie-artikelen).
|
||||
|
||||
3. TITEL-ANKER: staat er geen enkel OV-signaal in de titel (geen
|
||||
familie-hit, geen whitelist-term), dan is de score uitsluitend
|
||||
samenvatting-bewijs en mag die geen zwak/ambigue woord bevatten —
|
||||
'station'+'pendelbus'+'bus' in een golfverslag, 'openbaar
|
||||
vervoer'+'zakelijke reizigers' in een economiebericht en 'sterk +
|
||||
zwak uitsluitend in de samenvatting' (beide besluiten 2026-09-12)
|
||||
vallen hieronder. Twee sterke termen in de samenvatting blijven
|
||||
bewust door (delftopzondag 'tram 1, halte Buitenhofdreef',
|
||||
besluit 2026-09-13).
|
||||
|
||||
Weert zo 'VW-bus' (bus matcht niet ná een koppelteken), 'schoolbus',
|
||||
'bussen rijden om' in een woningbrand-lead, het werkwoord 'brengen',
|
||||
het idioom 'goed met elkaar overweg kunnen', 'een wijk opwaarderen',
|
||||
brandweerjargon 'sein brandmeester', figuratief 'weer op de rails',
|
||||
en een cultuurstukje over een 'stationspiano'. Bewust doorgelaten:
|
||||
krant 'De Metro'/café 'De Tram' (2, combineert) en atleet-'sprinter'
|
||||
in combinatie met ander OV-woord."""
|
||||
t_hits = RSSBot._fam_hits(title)
|
||||
s_hits = RSSBot._fam_hits(summary)
|
||||
# Eén familie telt 1× per ARTIKEL (titel én samenvatting samen): 'busstation'
|
||||
# in beide velden, of 60× in de tekst, blijft één signaal (besluit 2026-09-11).
|
||||
# _fam_hits levert per term al het hoogste gewicht bij overlappende families.
|
||||
# Unieke OV-signalen over beide velden: één term telt één signaal per
|
||||
# artikel ('busstation' in titel én samenvatting, of 60×, blijft één
|
||||
# treffer); _fam_hits levert per term al het hoogste gewicht bij
|
||||
# overlappende familie-treffers.
|
||||
combined = dict(t_hits + s_hits)
|
||||
score = sum(combined.values())
|
||||
whitelist_title = RSSBot._whitelist_hit(title)
|
||||
if whitelist_title:
|
||||
# Whitelist-term in de titel = +2 (geen auto-pass, max-2-stelsel
|
||||
# 2026-09-12): houdt het titel-anker voor onmiskenbare termen, maar
|
||||
# een enkele whitelist-hit laat nooit meer solo door.
|
||||
strong_title = RSSBot._whitelist_hit(title) or RSSBot._disruption_hit(title)
|
||||
if strong_title:
|
||||
# Stap 1: sterk titel-signaal — +2 bovenop het familie-gewicht van
|
||||
# dezelfde term, waardoor een eenduidige OV-titel alleen voldoet.
|
||||
score += 2
|
||||
if score < MATCH_FILTER_THRESHOLD:
|
||||
return False
|
||||
if max(combined.values(), default=0) == 1:
|
||||
# Alleen zwakke families: dan moeten ≥2 verschillende families meetellen
|
||||
# én een titel-anker (familie-hit óf whitelist-term in de titel).
|
||||
# Zwakke woorden in de samenvatting alleen zijn geen bewijs
|
||||
# ('station' + 'pendelbus' + 'bus' in een golfverslag telt anders op
|
||||
# tot de drempel).
|
||||
if not whitelist_title and (not t_hits or len(combined) < 2):
|
||||
return False
|
||||
elif not t_hits and not whitelist_title and 1 in combined.values():
|
||||
# Sterk (2) + zwak uitsluitend in de samenvatting, niets in de titel:
|
||||
# geen OV-bewijs. 'openbaar vervoer' + 'zakelijke reizigers' in een
|
||||
# economiebericht telt anders op tot de drempel (besluit 2026-09-12).
|
||||
if not t_hits and not strong_title and 1 in combined.values():
|
||||
# Stap 3: geen enkel titel-signaal én een zwak woord in de score —
|
||||
# samenvatting-bewijs zonder titel-anker is geen OV-bewijs.
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
@ -553,27 +579,24 @@ class RSSBot(Plugin):
|
|||
def _filter_summary() -> str:
|
||||
"""Mensvriendelijke weergave van het vaste gewogen match-filter."""
|
||||
return (
|
||||
"**OV-matchfilter (vast in code, gewogen families, max-2):**\n"
|
||||
"- Sterk (2): vervoerders (NS, Arriva, ProRail, Qbuzz, Deutsche Bahn, TESO, ...), spoorinfra (spoorlijn, spoorstoring, perron, spoorsector, ertms), "
|
||||
"samenstellingen als bushalte/buslijn/buschauffeur, treinstoring/treinverkeer/treinvertraging, "
|
||||
"intercity/streekvervoer/veerdienst/buurtbus/belbus, 'lijn 12', ICE-frasen, rangeer... — één hit volstaat NIET: "
|
||||
"moet combineren (max-2-stelsel, besluit 2026-09-12)\n"
|
||||
"- Middel (2): tram, metro, openbaar vervoer, lightrail, halte, pont, spoorbrug, ov-chipkaart/ovpay, ov-staking, conducteur, "
|
||||
"trein-/bus-/metrostation — zelfde regel: moet combineren met een andere OV-familie; "
|
||||
"sterk/middel + zwak uitsluitend in de samenvatting (geen hit in de titel) telt niet\n"
|
||||
"- Zwak (1): losse 'bus' (niet ná een koppelteken), 'pendelbus'/'shuttlebus', 'trein', 'station(s)', 'breng', 'overweg', 'overwegen', "
|
||||
"'sein', 'kaartje', 'opwaarderen', 'rails', 'inchecken'/'uitchecken', 'dienstregeling', 'chipkaart', 'reisproduct', 'vervoersbewijs', "
|
||||
"'ontsporen', 'reiziger'/'passagier', 'vervoerder', 'sprinter', 'spoor', 'staking', 'vervoer', 'busongeluk', ... — moet combineren met een ander OV-woord, met minstens één zwak woord in de titel\n\n"
|
||||
"**Whitelist (+2 bij een hit in de TITEL, geen auto-pass):** onmiskenbare OV-termen "
|
||||
"(NS, ProRail, Nederlandse Spoorwegen, spoorstoring, perron, bushalte, conducteur, "
|
||||
"ov-chipkaart, ovpay, openbaar vervoer, Deutsche Bahn, ICE Berlijn, ...); in de samenvatting alleen weegt zo'n term mee "
|
||||
"als gewogen familie\n\n"
|
||||
"Uitgesloten (blacklist): woorden die wél matchen maar geen OV-waarde hebben, "
|
||||
"o.a. straatnamen rond stations ('stationsstraat', 'stationsweg', ...) en niet-OV-vormen als 'stationcar' en 'stationair'\n\n"
|
||||
"Doorlaten: som van unieke OV-families ≥ 3 — één familie telt 1× per artikel "
|
||||
"(titel + samenvatting samen; herhaling of beide velden = één signaal); "
|
||||
"bij uitsluitend zwakke woorden tellen alleen "
|
||||
"≥2 verschillende families én een zwak woord in de titel"
|
||||
"**OV-matchfilter (vast in code; max-2-stelsel, precision-first):**\n"
|
||||
"- **Sterk signaal in de TITEL → door**: whitelist-term (NS, ProRail, Nederlandse Spoorwegen, spoorstoring, perron, "
|
||||
"bushalte, conducteur, ov-chipkaart, ovpay, openbaar vervoer, Deutsche Bahn, ICE Berlijn, ...) of een dienststoring-zin "
|
||||
"('sprinters rijden niet', 'metro rijdt niet') — die telt +2 bovenop het familie-gewicht. In de samenvatting alleen "
|
||||
"weegt zo'n term mee als gewone familie.\n"
|
||||
"- **Context (som ≥ 3)**: unieke OV-families over titel + samenvatting. Sterk (2): vervoerders (NS, Arriva, ProRail, Qbuzz, ...), "
|
||||
"spoorinfra (spoorlijn, spoorstoring, perron, ertms, spoorsector), bus-samenstellingen (bushalte, buslijn, buschauffeur, busbaan), "
|
||||
"treinstoring/treinverkeer/treinvertraging, intercity/streekvervoer/veerdienst/buurtbus/belbus, 'lijn 12', ICE-frasen, rangeer..., "
|
||||
"tram, metro, openbaar vervoer, lightrail, halte, pont, spoorbrug, ov-chipkaart/ovpay, ov-staking, conducteur, dienststoring-zin. "
|
||||
"Zwak (1): losse 'bus' (niet ná een koppelteken), 'pendelbus'/'shuttlebus', 'trein', 'station(s)', 'breng', 'overweg', "
|
||||
"'overwegen', 'sein', 'kaartje', 'opwaarderen', 'rails', 'inchecken'/'uitchecken', 'dienstregeling', 'chipkaart', "
|
||||
"'reisproduct', 'vervoersbewijs', 'ontsporen', 'reiziger'/'passagier', 'vervoerder', 'sprinter', 'spoor', 'staking', "
|
||||
"'vervoer', 'busongeluk', 'busvervoer', ... — elke dubbelzinnige term heeft een eigen familie en moet combineren.\n"
|
||||
"- **Eén term = één signaal**: herhaling, beide velden en overlappende familie-treffers wegen niet dubbel. "
|
||||
"Zonder enig titel-signaal (familie-hit, whitelist-term of storingszin in de titel) mag de score geen zwak woord "
|
||||
"bevatten — samenvatting-bewijs is geen OV-bewijs (twee sterke termen in de samenvatting blijven bewust door).\n"
|
||||
"- **Uitgesloten (blacklist)**: 'treinstraat'/'treinweg' (straatnamen rond spoorlijnen); historische stations*-gevallen "
|
||||
"('stationsstraat', 'stationair', 'stationcar', ...) worden al door de woordgrens-familie geweerd"
|
||||
)
|
||||
|
||||
async def _send(
|
||||
|
|
|
|||
Loading…
Reference in a new issue