From 1346cbb5494546903eb74601b9e2726747f12fdb Mon Sep 17 00:00:00 2001 From: Eddy de Vink Date: Sun, 13 Sep 2026 08:37:17 +0200 Subject: [PATCH] =?UTF-8?q?v1.13.0:=20=C3=A9=C3=A9n=20term=20=3D=20=C3=A9?= =?UTF-8?q?=C3=A9n=20signaal=20=E2=80=94=20overlappende=20familie-treffers?= =?UTF-8?q?=20wegen=201=C3=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'busbaan' stond in de bus-samenstelling-familie (fam 4) én de halte/busbaan-familie (fam 13): één woord scoorde 2+2=4 en liet zo een niet-OV-bericht door (Omroep Brabant politie-achtervolging 'via de busbaan', 13-09-2026). Latente dubbeltellingen: 'ov-chipkaart' bevat 'chipkaart', 'openbaar vervoer' bevat 'vervoer'. _fam_hits voegt nu overlappende span van verschillende families samen met het hoogste gewicht (retour blijft (familie-index, gewicht), dus de familie-één-signaal-regel over titel+samenvatting is intact). Beleidscommentaar + regressie (56 cases) bijgewerkt; 45-dagen-replay (90 geposte entries): precies 1 gedragsverschil — het fatbikers- bericht wordt nu geweerd, delftopzondag (tram+halte, twee termen) blijft door (besluit 2026-09-13). --- buzzrss/bot.py | 50 ++++++++++++++++++++++++++++++++++++---------- maubot.yaml | 2 +- validate_filter.py | 12 +++++++++++ 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/buzzrss/bot.py b/buzzrss/bot.py index 8b19018..d9cdac1 100644 --- a/buzzrss/bot.py +++ b/buzzrss/bot.py @@ -55,8 +55,11 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT # Gewogen OV-trefwoord-families voor het match-filter. Elke tuple = (regex, gewicht). # 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). Drempel (3): doorlaten als de som van de unieke familie-gewichten -# >= MATCH_FILTER_THRESHOLD (titel weegt niet extra). +# 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). # 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). @@ -118,6 +121,10 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT # 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). # - sterk (2): onmiskenbaar OV-jargon/merken/samenvoegingen (spoorlijn, # bushalte, busverkeer, intercity, veerboot, ertms, # rangeer..., ice-frasen, ...). Eén hit (titel óf samenvatting) @@ -175,7 +182,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"(? list[tuple[int, int]]: - """(familie-index, gewicht) voor elke OV-familie die in text voorkomt (1× per familie). + """(familie-index, gewicht) per OV-signaal in text (1× per familie). Gematchte woorden uit MATCH_FILTER_BLACKLIST (o.a. 'stationsstraat') tellen niet mee en blokkeren de familie niet: een écht OV-woord in - dezelfde tekst scoort gewoon verder.""" - hits: list[tuple[int, int]] = [] + dezelfde tekst scoort gewoon verder. + + 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.""" + 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): if m.group(0).lower() in MATCH_FILTER_BLACKLIST: continue - hits.append((idx, weight)) + spans.append((m.start(), m.end(), idx, weight)) break - return hits + spans.sort() + merged: list[tuple[int, int, int, int]] = [] + for start, end, idx, weight in spans: + if merged and start < merged[-1][1]: + s0, e0, i0, w0 = merged[-1] + if weight > w0: + merged[-1] = (s0, max(e0, end), idx, weight) + else: + merged[-1] = (s0, max(e0, end), i0, w0) + else: + merged.append((start, end, idx, weight)) + return [(idx, w) for _, _, idx, w in merged] @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). Doorlaten bij som >= drempel (3). Geen enkele familie weegt + éé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 @@ -494,6 +523,7 @@ class RSSBot(Plugin): 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. combined = dict(t_hits + s_hits) score = sum(combined.values()) whitelist_title = RSSBot._whitelist_hit(title) diff --git a/maubot.yaml b/maubot.yaml index 84064ec..3755ba0 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -1,5 +1,5 @@ id: nl.eddydevink.rss -version: 1.12.0 +version: 1.13.0 modules: - buzzrss main_class: RSSBot diff --git a/validate_filter.py b/validate_filter.py index d82c66c..2c54614 100755 --- a/validate_filter.py +++ b/validate_filter.py @@ -213,6 +213,18 @@ GOLDEN = [ ("Meerdere doden na busongeluk in Zwitserland", "Het busongeluk gebeurde naast de snelweg bij een afrit.", False), # busongeluk(1) alleen = zwak-alleen ("Antwerpen ontvangt eerste ICE", "De eerste ICE van Deutsche Bahn rijdt vanaf volgende maand naar Antwerpen.", False), # bewust geweerd: 'ice van' is NIET opgenomen (ijs-botsing); deutsche bahn(2) alleen < 3 — was vóór de wijziging óók al geweerd ("NS verlengt noodregeling tot volgend jaar", "De vervoerder kampt met personeelstekorten.", True), # ns(2) + vervoerder(1) = 3 + # fix 2026-09-13: één term mag nooit via twee families dubbel wegen. + # Echte case: Omroep Brabant (13-09-2026) — politie-achtervolging van fatbikers + # 'via de busbaan' scoorde 2+2=4 omdat 'busbaan' in de bus-samenstelling-familie + # (fam 4) én in de halte/busbaan-familie (fam 13) staat. Nu één signaal (2): + # zonder tweede OV-term onder de drempel. + ("Groep jonge fatbikers door Eindhoven achtervolgd na beroving met geweld", + "Agenten reden met zwaailichten achter de groep aan, onder meer via de busbaan en een fietspad.", False), + ("De politie achtervolgde de groep via de busbaan", "De achtervolging duurde enkele minuten.", True), # whitelist-titel-anker: 'busbaan' in de TITEL blijft door (+2); de merge raakt alleen de teller + # recall bewaakt: echt busbaan-nieuws blijft door (whitelist-titel-anker +2) en + # twee VERSCHILLENDE termen blijven samen tellen (geen over-merge). + ("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 ] print(f"== regressie: {len(GOLDEN)} cases (precision-first) ==") for t, s, want in GOLDEN: