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
|
# 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
|
# 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
|
# 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,
|
# gewicht wint): 'ov-chipkaart' bevat 'chipkaart', 'openbaar vervoer' bevat
|
||||||
# 'ov-chipkaart' bevat 'chipkaart', 'openbaar vervoer' bevat 'vervoer' — geen
|
# 'vervoer', 'treinstoring' matcht de zwakke \w*trein\w*-familie mee — geen
|
||||||
# dubbeltelling (besluit 2026-09-13). Drempel (3): doorlaten als de som van de
|
# dubbeltelling (besluit 2026-09-13; 'busbaan' stond in twee families en is
|
||||||
# unieke familie-gewichten >= MATCH_FILTER_THRESHOLD (titel weegt niet extra).
|
# 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
|
# 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
|
# doorlaten (2 < 3); écht OV-nieuws combineert in de praktijk altijd met een
|
||||||
# tweede signaal (gevalideerd op 205 productie-artikelen; zie beleidsblok).
|
# 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
|
# Doel: precisie boven recall. Liever een OV-bericht te veel gefilterd dan
|
||||||
# een niet-OV-bericht te veel doorgelaten (gebruikersbesluit, 2026-09-10).
|
# een niet-OV-bericht te veel doorgelaten (gebruikersbesluit, 2026-09-10).
|
||||||
#
|
#
|
||||||
# Lagen, elk met een eigen regel (MAX-2-stelsel, besluit 2026-09-12):
|
# Beslissing, in drie stappen (refactor 2026-09-13; logica equivalent met de
|
||||||
# 1. WHITELIST -> termen die in élke context OV betekenen (merken:
|
# 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,
|
# NS/ProRail/Arriva; onmiskenbaar jargon: spoorstoring, perron, bushalte,
|
||||||
# conducteur, ovpay; multi-woord-frasen: nederlandse spoorwegen, openbaar
|
# conducteur, ovpay; multi-woord-frasen: nederlandse spoorwegen, openbaar
|
||||||
# vervoer, ice-berlijn ...). Een whitelist-hit in de TITEL telt +2 (geen
|
# vervoer, ice-berlijn ...). Een whitelist-hit in de titel telt +2,
|
||||||
# auto-pass meer). Omdat elke whitelist-term óók in een familie zit, is dat
|
# bovenop het familie-gewicht van dezelfde term (elke whitelist-term zit
|
||||||
# effectief het oude titel-anker voor onmiskenbare termen — 'Reactie
|
# óók in een familie) — samen altijd >= drempel, dus eenduidige OV-titels
|
||||||
# Nederlandse Spoorwegen' blijft daardoor door. REGEL: twijfel je of de term
|
# ('Reactie Nederlandse Spoorwegen', 'Werkzaamheden aan perron') passeren
|
||||||
# ook buiten het OV kan voorkomen -> NIET hier, maar zwak (eigen familie).
|
# alleen. REGEL: twijfel je of de term ook buiten het OV kan voorkomen ->
|
||||||
# Een whitelist-term ALLEEN in de SAMENVATTING blijft inert: een bijzin als
|
# NIET hier, maar zwak (eigen familie). Een whitelist-term ALLEEN in de
|
||||||
# 'de zakelijke bezoeker is ook nodig voor het openbaar vervoer' in een
|
# SAMENVATTING blijft inert: een bijzin als 'de zakelijke bezoeker is ook
|
||||||
# economiebericht is geen OV-nieuws (besluit 2026-09-12); zo'n hit scoort
|
# nodig voor het openbaar vervoer' in een economiebericht is geen
|
||||||
# alleen via de overeenkomstige familie.
|
# OV-nieuws (besluit 2026-09-12); zo'n hit scoort alleen via de familie.
|
||||||
# 2. GEWOGEN FAMILIES -> score = som van unieke OV-families over het hele
|
# 2. CONTEXT: som van unieke OV-signalen over titel + samenvatting
|
||||||
# artikel (titel + samenvatting; per familie 1× per artikel — herhaling of
|
# >= drempel (3). Per familie 1× per artikel (herhaling of beide velden
|
||||||
# beide velden tellen niet dubbel, besluit 2026-09-11) >= drempel (3).
|
# tellen niet dubbel, besluit 2026-09-11); overlappende familie-treffers
|
||||||
# Overlappende treffers van verschillende families (één term op één plek)
|
# van dezelfde term wegen 1× met het hoogste gewicht (span-merge in
|
||||||
# tellen samen als 1× met het hoogste gewicht — een term mag nooit dubbel
|
# _fam_hits, besluit 2026-09-13 — 'ov-chipkaart' bevat 'chipkaart' maar
|
||||||
# wegen ('busbaan' in de bus-samenstelling- én de halte/busbaan-familie,
|
# weegt als één term 2, geen 3).
|
||||||
# besluit 2026-09-13).
|
|
||||||
# - sterk (2): onmiskenbaar OV-jargon/merken/samenvoegingen (spoorlijn,
|
# - sterk (2): onmiskenbaar OV-jargon/merken/samenvoegingen (spoorlijn,
|
||||||
# bushalte, busverkeer, intercity, veerboot, ertms,
|
# bushalte, busverkeer, intercity, veerboot, ertms,
|
||||||
# rangeer..., ice-frasen, ...). Eén hit (titel óf samenvatting)
|
# rangeer..., ice-frasen, ...) én het dienststoring-
|
||||||
# is onvoldoende: moet combineren (2 + 1 = 3). De vroegere
|
# contextpatroon 'vervoerswoord + rijdt/rijden (niet) +
|
||||||
# 'één sterk woord volstaat'-regel is vervallen — 'hermes'
|
# niet' ('sprinters rijden niet', 'metro rijdt niet').
|
||||||
# als achternaam passeerde zo op één vervoerder-hit
|
# Eén hit (titel óf samenvatting) is onvoldoende: moet
|
||||||
# (Margraten, 2026-09-12).
|
# combineren (2 + 1 = 3). De vroegere 'één sterk woord
|
||||||
# EXTRA (besluit 2026-09-12): een sterk/medium + zwak-
|
# volstaat'-regel is vervallen — 'hermes' als achternaam
|
||||||
# combinatie UITSLUITEND in de samenvatting (geen enkele hit
|
# passeerde zo op één vervoerder-hit (Margraten,
|
||||||
# in de titel én geen whitelist-term in de titel) is geen
|
# 2026-09-12).
|
||||||
# OV-bewijs — 'openbaar vervoer' + 'zakelijke reizigers' in
|
|
||||||
# een economiebericht telt anders op tot de drempel.
|
|
||||||
# - zwak (1): DUBBELZINNIGHEID. Woorden die (ook) buiten het OV
|
# - zwak (1): DUBBELZINNIGHEID. Woorden die (ook) buiten het OV
|
||||||
# voorkomen. REGEL: elk dubbelzinnig woord krijgt een EIGEN
|
# voorkomen. REGEL: elk dubbelzinnig woord krijgt een EIGEN
|
||||||
# familie (niet samengevoegd), zodat twee verschillende
|
# 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,
|
# trein, bus, station(s), sprinter, reiziger/passagier,
|
||||||
# vervoerder, inchecken/uitchecken, sein, overweg, breng,
|
# vervoerder, inchecken/uitchecken, sein, overweg, breng,
|
||||||
# kaartje, opwaarderen, rails, dienstregeling, chipkaart,
|
# kaartje, opwaarderen, rails, dienstregeling, chipkaart,
|
||||||
# ontsporen, pendelbus/shuttlebus (evenement-/hotel-/
|
# reisproduct, vervoer(s)bewijs, ontsporen,
|
||||||
# parkeerpendel; synoniemen -> bewust één familie),
|
# pendelbus/shuttlebus (evenement-/hotel-/parkeerpendel;
|
||||||
# busongeluk/busvervoer (touringcar/schoolbus), staking,
|
# synoniemen -> bewust één familie), busongeluk/busvervoer
|
||||||
# vervoer, vervoersbedrijf, vervoerregio, ritten,
|
# (touringcar/schoolbus), staking, vervoer, vervoersbedrijf,
|
||||||
# bovenleiding (industrie), abonnement, hermes, spoor.
|
# vervoerregio, ritten, bovenleiding (industrie),
|
||||||
# EXTRA REGEL voor zwak-alleen: staat er uitsluitend zwak
|
# abonnement, hermes, spoor.
|
||||||
# bewijs in de SAMENVATTING (geen enkele zwakke hit in de
|
# 3. TITEL-ANKER: staat er geen enkel OV-signaal in de titel (geen
|
||||||
# titel), dan niet doorlaten. Twee/meer zwakke woorden in
|
# familie-hit, geen whitelist-term), dan is de score uitsluitend
|
||||||
# de samenvatting zijn anders samen al 3 punten
|
# samenvatting-bewijs en mag die geen zwak woord bevatten. Twee/meer
|
||||||
# ("station" + "pendelbus" + "bus" in een golfverslag).
|
# zwakke woorden in de samenvatting ('station' + 'pendelbus' + 'bus' in
|
||||||
# 3. BLACKLIST -> gematchte woorden zonder OV-waarde (stationsstraat,
|
# een golfverslag) en sterk+zwak uitsluitend in de samenvatting
|
||||||
# stationair, stationcar, ...); blokkeren de familie niet.
|
# ('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):
|
# BEOORDELEN VAN EEN NIEUW WOORD (incl. COMPLETEHEIDS-AUDIT, 2026-09-12):
|
||||||
# a) Kan het ook buiten het OV voorkomen? ja -> zwak (1), eigen familie.
|
# 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,
|
# spoorvervoerder, treinvertraging/uitval, ertms, teso, rangeer, ice-frasen,
|
||||||
# maaslijn, ns international, deutsche bahn, european sleeper, italo,
|
# maaslijn, ns international, deutsche bahn, european sleeper, italo,
|
||||||
# nightjet (sterk) en 11 zwakke woorden op.
|
# 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]] = [
|
MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
|
||||||
# sterk (2) — onmiskenbaar OV, maar één hit is onvoldoende (max-2-stelsel,
|
# 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(?: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"\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"(?<![\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(?: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\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
|
(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)
|
# onmiskenbaar OV maar iets algemener (2, moet ook combineren)
|
||||||
(r"\btrams?\b", 2),
|
(r"\btrams?\b", 2),
|
||||||
(r"\bmetro(?:'?s)?\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-chipkaart|ovpay|ov-pay|studentenreisproduct)\w*\b", 2),
|
||||||
(r"\b(?:ov-staking|conducteursstaking)\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"\bconduct(?:eur|eurs)\b", 2), # trein-/buspersoneel: onmiskenbaar OV-jargon
|
||||||
(r"\b(?:pont|ponten)\b", 2), # waterbus/veerdienst
|
(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
|
(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
|
# 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:
|
# hierin staat telt niet mee voor de score (en blokkeert de familie niet). Vorm:
|
||||||
# exact woord (lowercase), zoals door de familie-regex wordt gematcht.
|
# 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({
|
MATCH_FILTER_BLACKLIST: frozenset[str] = frozenset({
|
||||||
# Straatnamen rond stations (genitief-s-vorm van "\bstation\w*\b"): een straat
|
# Straatnamen rond spoorlijnen die de \b\w*trein\w*\b-familie zou matchen:
|
||||||
# die toevallig bij een station ligt maakt een bericht nog geen OV-bericht.
|
# een straat die toevallig 'Trein' heet maakt een bericht 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".
|
|
||||||
"treinstraat", "treinweg",
|
"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
|
# 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)
|
_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):
|
class Config(BaseProxyConfig):
|
||||||
def do_update(self, helper: ConfigUpdateHelper) -> None:
|
def do_update(self, helper: ConfigUpdateHelper) -> None:
|
||||||
|
|
@ -447,6 +467,14 @@ class RSSBot(Plugin):
|
||||||
low = text.lower()
|
low = text.lower()
|
||||||
return any(ph in low for ph in _WHITELIST_PHRASES)
|
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
|
@staticmethod
|
||||||
def _fam_hits(text: str) -> list[tuple[int, int]]:
|
def _fam_hits(text: str) -> list[tuple[int, int]]:
|
||||||
"""(familie-index, gewicht) per OV-signaal in text (1× per familie).
|
"""(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
|
Eén term telt maximaal één signaal (besluit 2026-09-13): overlappende
|
||||||
treffers van verschillende families op dezelfde plek worden samengevoegd
|
treffers van verschillende families op dezelfde plek worden samengevoegd
|
||||||
met het hoogste gewicht. Voorheen telde één woord dubbel als het in
|
met het hoogste gewicht. Voorheen telde één woord dubbel als het in
|
||||||
meerdere families stond — 'busbaan' zit in de bus-samenstelling-familie
|
meerdere families stond — 'ov-chipkaart' bevat 'chipkaart' (2+1=3),
|
||||||
én in de halte/busbaan-familie (2+2=4), 'ov-chipkaart' bevat 'chipkaart'
|
'openbaar vervoer' bevat 'vervoer' (2+1=3), 'treinstoring' matcht ook
|
||||||
(2+1=3), 'openbaar vervoer' bevat 'vervoer' (2+1=3). Max-2-stelsel:
|
de zwakke \\w*trein\\w*-familie (2+1=3). Max-2-stelsel: één term kan
|
||||||
één term kan nooit solo doorlaten."""
|
nooit solo doorlaten."""
|
||||||
spans: list[tuple[int, int, int, int]] = [] # (start, end, familie-idx, gewicht)
|
spans: list[tuple[int, int, int, int]] = [] # (start, end, familie-idx, gewicht)
|
||||||
for idx, (family, weight) in enumerate(_compiled_match_families):
|
for idx, (family, weight) in enumerate(_compiled_match_families):
|
||||||
for m in family.finditer(text):
|
for m in family.finditer(text):
|
||||||
|
|
@ -484,68 +512,66 @@ class RSSBot(Plugin):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _filter_passes(title: str, summary: str) -> bool:
|
def _filter_passes(title: str, summary: str) -> bool:
|
||||||
"""Gewogen familiescore (max-2-stelsel, besluit 2026-09-12): elke OV-familie
|
"""Driestaps-beslissing, precision-first (refactor 2026-09-13; beslissing
|
||||||
telt 1× per artikel (titel + samenvatting samen; herhaling of beide velden =
|
equivalent met v1.13.0, maar de drie historische uitzonderings-takken
|
||||||
één signaal). Overlappende treffers van verschillende families tellen samen
|
reduceren tot één regel — zie stap 3):
|
||||||
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).
|
|
||||||
|
|
||||||
Whitelist-termen (onmiskenbaar OV in élke context) tellen in de TITEL +2 —
|
1. STERK SIGNAAL: een whitelist-term óf een dienststoring-zin
|
||||||
géén auto-pass meer; omdat elke whitelist-term óók in een familie zit, is
|
('sprinters rijden niet') in de titel is voldoende. Elke
|
||||||
dat effectief het behouden titel-anker ('Reactie Nederlandse Spoorwegen',
|
whitelist-term staat óók in een familie, dus zo'n titel telt +2 én
|
||||||
'Werkzaamheden aan perron', 'Buurtbus rijdt niet meer' blijven door). Een
|
het familie-gewicht — samen altijd >= drempel ('NS meldt storing',
|
||||||
whitelist-term ALLEEN in de samenvatting is geen OV-bewijs (besluit
|
'Werkzaamheden aan perron', 'Buurtbus rijdt niet meer'). In de
|
||||||
2026-09-12: 'openbaar vervoer' als bijzin in een economiebericht) en scoort
|
samenvatting ALLEEN is zo'n term géén bewijs ('openbaar vervoer'
|
||||||
alleen via de overeenkomstige familie.
|
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',
|
2. CONTEXT: de som van unieke OV-signalen over titel + samenvatting
|
||||||
'opwaarderen', 'kaartje', 'rails', 'pendelbus', 'spoor', 'staking',
|
>= drempel (3). Eén term telt één signaal per artikel ('busstation'
|
||||||
'vervoer', 'busongeluk', ...) moeten combineren met een ander OV-woord én
|
60× of in beide velden = één keer); overlappende familie-treffers
|
||||||
minstens één zwak woord moet in de titel staan (of een whitelist-term in de
|
van dezelfde term wegen 1× met het hoogste gewicht (span-merge in
|
||||||
titel); dezelfde zwakke familie in titel én samenvatting is géén combinatie.
|
_fam_hits, besluit 2026-09-13 — 'ov-chipkaart' bevat 'chipkaart'
|
||||||
Een sterk (2) + zwak-combinatie UITSLUITEND in de samenvatting (geen
|
maar weegt als één term 2, geen 3). Geen
|
||||||
familie-hit én geen whitelist-term in de titel) is geen OV-bewijs
|
familie weegt meer dan 2 (max-2-stelsel, besluit 2026-09-12): een
|
||||||
('openbaar vervoer' + 'zakelijke reizigers' in een economiebericht, besluit
|
enkel sterk woord kan nooit solo doorlaten ('hermes' als
|
||||||
2026-09-12). Weert oa 'VW-bus' (bus matcht niet ná een koppelteken),
|
achternaam passeerde vroeger op één vervoerder-hit van 3 —
|
||||||
'schoolbus', 'bussen rijden om' in een woningbrand-lead, het werkwoord
|
Margraten; écht OV-nieuws combineert in de praktijk altijd met een
|
||||||
'brengen', het idioom 'goed met elkaar overweg kunnen', 'een wijk
|
tweede signaal, gevalideerd op 205 productie-artikelen).
|
||||||
opwaarderen', brandweerjargon 'sein brandmeester', figuratief 'weer op de
|
|
||||||
rails', een golfverslag waarin de samenvatting 'station' + 'pendelbus' +
|
3. TITEL-ANKER: staat er geen enkel OV-signaal in de titel (geen
|
||||||
'bus' noemt, en het Margraten-bericht met 'Ton Hermes' als achternaam.
|
familie-hit, geen whitelist-term), dan is de score uitsluitend
|
||||||
Bewust doorgelaten: krant 'De Metro'/café 'De Tram' (2, combineert) en
|
samenvatting-bewijs en mag die geen zwak/ambigue woord bevatten —
|
||||||
atleet-'sprinter' in combinatie met ander OV-woord."""
|
'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)
|
t_hits = RSSBot._fam_hits(title)
|
||||||
s_hits = RSSBot._fam_hits(summary)
|
s_hits = RSSBot._fam_hits(summary)
|
||||||
# Eén familie telt 1× per ARTIKEL (titel én samenvatting samen): 'busstation'
|
# Unieke OV-signalen over beide velden: één term telt één signaal per
|
||||||
# in beide velden, of 60× in de tekst, blijft één signaal (besluit 2026-09-11).
|
# artikel ('busstation' in titel én samenvatting, of 60×, blijft één
|
||||||
# _fam_hits levert per term al het hoogste gewicht bij overlappende families.
|
# treffer); _fam_hits levert per term al het hoogste gewicht bij
|
||||||
|
# overlappende familie-treffers.
|
||||||
combined = dict(t_hits + s_hits)
|
combined = dict(t_hits + s_hits)
|
||||||
score = sum(combined.values())
|
score = sum(combined.values())
|
||||||
whitelist_title = RSSBot._whitelist_hit(title)
|
strong_title = RSSBot._whitelist_hit(title) or RSSBot._disruption_hit(title)
|
||||||
if whitelist_title:
|
if strong_title:
|
||||||
# Whitelist-term in de titel = +2 (geen auto-pass, max-2-stelsel
|
# Stap 1: sterk titel-signaal — +2 bovenop het familie-gewicht van
|
||||||
# 2026-09-12): houdt het titel-anker voor onmiskenbare termen, maar
|
# dezelfde term, waardoor een eenduidige OV-titel alleen voldoet.
|
||||||
# een enkele whitelist-hit laat nooit meer solo door.
|
|
||||||
score += 2
|
score += 2
|
||||||
if score < MATCH_FILTER_THRESHOLD:
|
if score < MATCH_FILTER_THRESHOLD:
|
||||||
return False
|
return False
|
||||||
if max(combined.values(), default=0) == 1:
|
if not t_hits and not strong_title and 1 in combined.values():
|
||||||
# Alleen zwakke families: dan moeten ≥2 verschillende families meetellen
|
# Stap 3: geen enkel titel-signaal én een zwak woord in de score —
|
||||||
# én een titel-anker (familie-hit óf whitelist-term in de titel).
|
# samenvatting-bewijs zonder titel-anker is geen OV-bewijs.
|
||||||
# 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).
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -553,27 +579,24 @@ class RSSBot(Plugin):
|
||||||
def _filter_summary() -> str:
|
def _filter_summary() -> str:
|
||||||
"""Mensvriendelijke weergave van het vaste gewogen match-filter."""
|
"""Mensvriendelijke weergave van het vaste gewogen match-filter."""
|
||||||
return (
|
return (
|
||||||
"**OV-matchfilter (vast in code, gewogen families, max-2):**\n"
|
"**OV-matchfilter (vast in code; max-2-stelsel, precision-first):**\n"
|
||||||
"- Sterk (2): vervoerders (NS, Arriva, ProRail, Qbuzz, Deutsche Bahn, TESO, ...), spoorinfra (spoorlijn, spoorstoring, perron, spoorsector, ertms), "
|
"- **Sterk signaal in de TITEL → door**: whitelist-term (NS, ProRail, Nederlandse Spoorwegen, spoorstoring, perron, "
|
||||||
"samenstellingen als bushalte/buslijn/buschauffeur, treinstoring/treinverkeer/treinvertraging, "
|
"bushalte, conducteur, ov-chipkaart, ovpay, openbaar vervoer, Deutsche Bahn, ICE Berlijn, ...) of een dienststoring-zin "
|
||||||
"intercity/streekvervoer/veerdienst/buurtbus/belbus, 'lijn 12', ICE-frasen, rangeer... — één hit volstaat NIET: "
|
"('sprinters rijden niet', 'metro rijdt niet') — die telt +2 bovenop het familie-gewicht. In de samenvatting alleen "
|
||||||
"moet combineren (max-2-stelsel, besluit 2026-09-12)\n"
|
"weegt zo'n term mee als gewone familie.\n"
|
||||||
"- Middel (2): tram, metro, openbaar vervoer, lightrail, halte, pont, spoorbrug, ov-chipkaart/ovpay, ov-staking, conducteur, "
|
"- **Context (som ≥ 3)**: unieke OV-families over titel + samenvatting. Sterk (2): vervoerders (NS, Arriva, ProRail, Qbuzz, ...), "
|
||||||
"trein-/bus-/metrostation — zelfde regel: moet combineren met een andere OV-familie; "
|
"spoorinfra (spoorlijn, spoorstoring, perron, ertms, spoorsector), bus-samenstellingen (bushalte, buslijn, buschauffeur, busbaan), "
|
||||||
"sterk/middel + zwak uitsluitend in de samenvatting (geen hit in de titel) telt niet\n"
|
"treinstoring/treinverkeer/treinvertraging, intercity/streekvervoer/veerdienst/buurtbus/belbus, 'lijn 12', ICE-frasen, rangeer..., "
|
||||||
"- Zwak (1): losse 'bus' (niet ná een koppelteken), 'pendelbus'/'shuttlebus', 'trein', 'station(s)', 'breng', 'overweg', 'overwegen', "
|
"tram, metro, openbaar vervoer, lightrail, halte, pont, spoorbrug, ov-chipkaart/ovpay, ov-staking, conducteur, dienststoring-zin. "
|
||||||
"'sein', 'kaartje', 'opwaarderen', 'rails', 'inchecken'/'uitchecken', 'dienstregeling', 'chipkaart', 'reisproduct', 'vervoersbewijs', "
|
"Zwak (1): losse 'bus' (niet ná een koppelteken), 'pendelbus'/'shuttlebus', 'trein', 'station(s)', 'breng', 'overweg', "
|
||||||
"'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"
|
"'overwegen', 'sein', 'kaartje', 'opwaarderen', 'rails', 'inchecken'/'uitchecken', 'dienstregeling', 'chipkaart', "
|
||||||
"**Whitelist (+2 bij een hit in de TITEL, geen auto-pass):** onmiskenbare OV-termen "
|
"'reisproduct', 'vervoersbewijs', 'ontsporen', 'reiziger'/'passagier', 'vervoerder', 'sprinter', 'spoor', 'staking', "
|
||||||
"(NS, ProRail, Nederlandse Spoorwegen, spoorstoring, perron, bushalte, conducteur, "
|
"'vervoer', 'busongeluk', 'busvervoer', ... — elke dubbelzinnige term heeft een eigen familie en moet combineren.\n"
|
||||||
"ov-chipkaart, ovpay, openbaar vervoer, Deutsche Bahn, ICE Berlijn, ...); in de samenvatting alleen weegt zo'n term mee "
|
"- **Eén term = één signaal**: herhaling, beide velden en overlappende familie-treffers wegen niet dubbel. "
|
||||||
"als gewogen familie\n\n"
|
"Zonder enig titel-signaal (familie-hit, whitelist-term of storingszin in de titel) mag de score geen zwak woord "
|
||||||
"Uitgesloten (blacklist): woorden die wél matchen maar geen OV-waarde hebben, "
|
"bevatten — samenvatting-bewijs is geen OV-bewijs (twee sterke termen in de samenvatting blijven bewust door).\n"
|
||||||
"o.a. straatnamen rond stations ('stationsstraat', 'stationsweg', ...) en niet-OV-vormen als 'stationcar' en 'stationair'\n\n"
|
"- **Uitgesloten (blacklist)**: 'treinstraat'/'treinweg' (straatnamen rond spoorlijnen); historische stations*-gevallen "
|
||||||
"Doorlaten: som van unieke OV-families ≥ 3 — één familie telt 1× per artikel "
|
"('stationsstraat', 'stationair', 'stationcar', ...) worden al door de woordgrens-familie geweerd"
|
||||||
"(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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _send(
|
async def _send(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
id: nl.eddydevink.rss
|
id: nl.eddydevink.rss
|
||||||
version: 1.13.0
|
version: 1.14.0
|
||||||
modules:
|
modules:
|
||||||
- buzzrss
|
- buzzrss
|
||||||
main_class: RSSBot
|
main_class: RSSBot
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,9 @@ WANT_ASSIGN = {
|
||||||
"MATCH_FILTER_FAMILIES", "MATCH_FILTER_THRESHOLD", "MATCH_FILTER_BLACKLIST",
|
"MATCH_FILTER_FAMILIES", "MATCH_FILTER_THRESHOLD", "MATCH_FILTER_BLACKLIST",
|
||||||
"MATCH_FILTER_WHITELIST", "_compiled_match_families",
|
"MATCH_FILTER_WHITELIST", "_compiled_match_families",
|
||||||
"_WHITELIST_RE", "_WHITELIST_PHRASES",
|
"_WHITELIST_RE", "_WHITELIST_PHRASES",
|
||||||
|
"_DISRUPTION_RE_SRC", "_DISRUPTION_RE",
|
||||||
}
|
}
|
||||||
WANT_METHODS = {"_fam_hits", "_filter_passes", "_filter_text", "_whitelist_hit"}
|
WANT_METHODS = {"_fam_hits", "_filter_passes", "_filter_text", "_whitelist_hit", "_disruption_hit"}
|
||||||
|
|
||||||
nodes = []
|
nodes = []
|
||||||
for n in tree.body:
|
for n in tree.body:
|
||||||
|
|
@ -116,6 +117,16 @@ for p, w in FAMILIES:
|
||||||
check(w in (1, 2), f"familie {p[:40]!r} heeft geldig gewicht {w} (max-2-stelsel)")
|
check(w in (1, 2), f"familie {p[:40]!r} heeft geldig gewicht {w} (max-2-stelsel)")
|
||||||
check(len(COMPILED) == len(FAMILIES), "alle families compileren")
|
check(len(COMPILED) == len(FAMILIES), "alle families compileren")
|
||||||
|
|
||||||
|
# --- Invariant 4 (refactor 2026-09-13): sterke-signaal-garantie + geen dode
|
||||||
|
# blacklist. Elke whitelist-term moet óók in een familie zitten (anders telt een
|
||||||
|
# whitelist-titel niet als sterk signaal 2+2) en elke zwartgelijste term moet door
|
||||||
|
# minstens één familie worden gematcht (anders is de blacklist-regel dood gewicht).
|
||||||
|
print("== invariant 4: whitelist-term in familie; blacklist-term door familie gematcht ==")
|
||||||
|
for w in sorted(ns["MATCH_FILTER_WHITELIST"]):
|
||||||
|
check(any(p.search(w) for p, _ in COMPILED), f"whitelist-term {w!r} zit ook in een familie")
|
||||||
|
for w in sorted(ns["MATCH_FILTER_BLACKLIST"]):
|
||||||
|
check(any(p.search(w) for p, _ in COMPILED), f"blacklist-term {w!r} wordt door een familie gematcht")
|
||||||
|
|
||||||
# --- Regressie: gedrag van _filter_passes op bekende cases (precision-first)
|
# --- Regressie: gedrag van _filter_passes op bekende cases (precision-first)
|
||||||
GOLDEN = [
|
GOLDEN = [
|
||||||
# (titel, samenvatting, verwacht_door)
|
# (titel, samenvatting, verwacht_door)
|
||||||
|
|
@ -225,6 +236,46 @@ GOLDEN = [
|
||||||
# twee VERSCHILLENDE termen blijven samen tellen (geen over-merge).
|
# twee VERSCHILLENDE termen blijven samen tellen (geen over-merge).
|
||||||
("Busbaan wordt afgesloten voor het doorgaande verkeer", "De omleiding duurt tot het voorjaar.", True),
|
("Busbaan wordt afgesloten voor het doorgaande verkeer", "De omleiding duurt tot het voorjaar.", True),
|
||||||
("Bus rijdt om, busbaan is afgesloten", "De bussen rijden via de wijk.", True), # bus(1) + busbaan(2) = 3
|
("Bus rijdt om, busbaan is afgesloten", "De bussen rijden via de wijk.", True), # bus(1) + busbaan(2) = 3
|
||||||
|
# refactor 2026-09-13 — positieve gevallen die de dienststoring-context én
|
||||||
|
# sterke signalen vastleggen (prompt "Starke OV-signalen").
|
||||||
|
("NS meldt storing: sprinters rijden niet", "De storing wordt verholpen.", True), # ns-titel + 'sprinters rijden niet'-context
|
||||||
|
("Metro rijdt niet tussen Rotterdam en Schiedam", "De metro's stoppen na de storing.", True), # 'metro rijdt niet'-context (2) + metro(2)
|
||||||
|
("Buslijn 12 vervalt tijdelijk", "De vervanging rijdt via de wijk.", True), # buslijn(2) + vervoer(1)
|
||||||
|
("Werkzaamheden aan perron", "Het perron wordt nieuwe maand opgeknapt.", True), # perron-titel-anker (2+2)
|
||||||
|
("Nieuwe bushalte geopend", "De halte ligt aan de rand van de wijk.", True), # bushalte-whitelist (2+2)
|
||||||
|
("Sprinters rijden niet tussen Utrecht en Amsterdam", "De oorzaak is nog onbekend.", True), # dienststoring-context(2) + sprinter(1) = 3 — prompt: echte sprinter-context
|
||||||
|
("Stoptreinen rijden niet naar Den Haag vanwege een storing", "De NS zet bussen in.", True), # 'stoptreinen rijden niet'(2) + trein(1) + ns(2)
|
||||||
|
# refactor 2026-09-13 — negatieve gevallen (prompt): ambigue woorden en
|
||||||
|
# straatnamen mogen niet zelfstandig voldoende zijn.
|
||||||
|
("Nieuwe woning bij station", "", False), # woning naast station: station(1) < 3
|
||||||
|
("Restaurant opent nieuwe vestiging", "Het restaurant ligt tegenover het station waar dagelijks veel reizigers komen.", False), # station+reizigers als bijzin — titel zegt niets over OV
|
||||||
|
("Stationstraat krijgt nieuwe woningen", "", False), # straatnaam-inert (oudere blacklist-entries verwijderd, refactor 2026-09-13)
|
||||||
|
("Werkzaamheden aan de stationsweg", "", False),
|
||||||
|
("Het stationsplein wordt heringericht voor de markt", "", False),
|
||||||
|
("De auto stond stationair te draaien", "", False),
|
||||||
|
("Nieuwe stationwagen van het merk", "", False),
|
||||||
|
("Stationcar uit de jaren zestig", "", False),
|
||||||
|
("De Metro publiceert vandaag een interview", "De krant verschijnt wekelijks.", False), # krant 'De Metro': metro(2) alleen
|
||||||
|
("Tram is een populaire naam voor het café", "", False), # café 'De Tram': tram(2) alleen
|
||||||
|
("Schoolbus betrokken bij ongeval", "De kinderen werden opgevangen.", False), # schoolbus: 0 OV-hits
|
||||||
|
("Bus en station tijdelijk gesloten voor het feest", "", False), # bus(1)+station(1) = 2 — geen auto-pass
|
||||||
|
("Wijkbewoners klagen over het station en de bussen", "De gemeente komt langs.", False), # station(1)+bussen(1)=2 — bus+station is geen auto-pass
|
||||||
|
("Passagiers klagen over de incheckbalie", "De balie opende te laat.", False), # passagiers(1)+inchecken(1) = 2
|
||||||
|
("Gemeente verhuist naar het stationsgebouw", "Het station en het stationsgebied worden vernieuwd.", False), # semantische stapel: station-varianten tellen niet als 4 signalen — alleen station(1) in samenvatting
|
||||||
|
# combinaties (prompt): twee onafhankelijke context-signalen zijn genoeg,
|
||||||
|
# tenzij de context niet-OV is (bovenstaande negatieve cases).
|
||||||
|
("Bus en tram krijgen in december een nieuwe dienstregeling", "", True), # bus(1)+dienstregeling(1)+tram(2) = 4 — echte OV-context van bus+dienstregeling
|
||||||
|
("Tram krijgt nieuwe dienstregeling", "De wijziging geldt vanaf december.", True), # tram(2)+dienstregeling(1) = 3
|
||||||
|
("De halte wordt verplaatst voor de bus", "De nieuwe halte komt bij de rotonde.", True), # halte(2)+bus(1) = 3
|
||||||
|
("Metro rijdt niet vanwege een storing", "Er is onbekend wanneer het rijden hervat.", True), # metro(2)+storing(1) = 3 (prompt: metro+storing)
|
||||||
|
("Trein rijdt niet meer op de spoorlijn naar Den Bosch", "Werkzaamheden duren tot maart.", True), # trein(1)+spoorlijn(2) = 3 (prompt: trein+spoorlijn)
|
||||||
|
("Sprinters rijden niet; NS zet bussen in", "", True), # sprinter(1)+rijden-niet(2)+ns(2) (prompt: sprinter+NS)
|
||||||
|
("De trams rijden niet door een storing", "Er zijn vervangende bussen ingezet.", True), # trams(2)+rijden-niet(2) (prompt: tram-storing)
|
||||||
|
("Tramverkeer stilgelegd na aanrijding", "", True), # tramverkeer-whitelist (prompt: tram-storing)
|
||||||
|
("ProRail meldt wisselstoring op traject Utrecht", "Het treinverkeer ligt stil.", True), # prorail-whitelist + treinverkeer(2)
|
||||||
|
("Treinstoring tussen Den Haag en Leiden", "Reizigers moeten rekening houden met vertraging.", True), # treinstoring-whitelist (prompt: treinstoring)
|
||||||
|
("Treinvertraging op het spoor bij Utrecht", "", True), # treinvertraging-whitelist (prompt: echte treinvertraging)
|
||||||
|
("NS past de dienstregeling aan", "Door werkzaamheden rijden er minder treinen.", True), # prompt-voorbeeld: ns + treinen
|
||||||
]
|
]
|
||||||
print(f"== regressie: {len(GOLDEN)} cases (precision-first) ==")
|
print(f"== regressie: {len(GOLDEN)} cases (precision-first) ==")
|
||||||
for t, s, want in GOLDEN:
|
for t, s, want in GOLDEN:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue