diff --git a/AGENTS.md b/AGENTS.md index ff6fd78..31bfb7a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,4 +16,11 @@ Maubot-plugin `nl.eddydevink.rss` (RSS → Matrix + WhatsApp), met een OV-nieuws (incl. `base-config.yaml`, anders `BaseMissingError`), upload met `allow_override=true`, daarna container-restart. Exacte procedure: `skill://maubot`. - **NOOIT** de wwebjs-container of WhatsApp-sessies (her)starten/beëindigen — meerdere bots delen die sessies. -- Productie draait het **geïnstalleerde mbp**, niet deze werkdirectory: een commit is pas actief na build+upload+restart. \ No newline at end of file +- Productie draait het **geïnstalleerde mbp**, niet deze werkdirectory: een commit is pas actief na build+upload+restart. + +## Bekend — NIET onderzoeken +- Na een deploy (upload + container-restart) staan er 2 verse `run complete`-regels in de run-log + binnen enkele seconden: maubot herstart de instance bij een nieuwe plugin-versie (upstream) én de + container-restart start opnieuw, elke start doet direct één poll. Dat is géén handler-lek en hoort + niet onderzocht te worden (besluit 2026-09-11). Alleen verdubbeling op élke poll-run is een + probleem. Zie `skill://maubot` → Veelgemaakte fouten. \ No newline at end of file diff --git a/base-config.yaml b/base-config.yaml index fc7fa59..af5b956 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -28,7 +28,7 @@ exclude_paywalled: true # 0 = uit. title_dedup_days: 30 # OV-matchfilter: VAST in code (gewogen families + blacklist), niet via config te wijzigen. -# Doorlaten als 2×(titelscore) + 1×(samenvattingscore) >= 3. Zie de families en de +# Doorlaten als som van unieke OV-families >= 3; één familie telt 1× per artikel (titel + samenvatting; herhaling of beide velden = 1 signaal). Zie de families en de # blacklist in buzzrss/bot.py (MATCH_FILTER_FAMILIES / MATCH_FILTER_BLACKLIST). # `!rss set-filter` en `!rss filter` tonen alleen de huidige (vaste) filter. # Users who can bypass room permission checks diff --git a/buzzrss/bot.py b/buzzrss/bot.py index 051630e..d431a2a 100644 --- a/buzzrss/bot.py +++ b/buzzrss/bot.py @@ -50,11 +50,13 @@ 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 tekst (niet per cluster/herhaling). Drempel (3): -# doorlaten als 2×(titelscore) + 1×(samenvattingscore) >= MATCH_FILTER_THRESHOLD +# 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). # Gewicht 3 (sterk, onmiskenbaar OV): één woord in titel óf samenvatting volstaat. -# Gewicht 2 (medium): één woord in de titel volstaat; in de samenvatting moet er -# een tweede OV-woord bijkomen. +# Gewicht 2 (medium): moet combineren met een andere OV-familie; één medium-hit, +# in titel óf samenvatting, haalt de drempel niet. # Gewicht 1 (zwak/dubbelzinnig: los "bus", "breng", "overweg", "overwegen", "sein", # "kaartje", "opwaarderen", "rails", "pendelbus"): moet met een ander OV-woord # combineren (een tweede, andere familie — dezelfde zwakke familie in titel én @@ -87,16 +89,34 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT # een niet-OV-bericht te veel doorgelaten (gebruikersbesluit, 2026-09-10). # # Drie lagen, elk met een eigen regel: -# 1. WHITELIST -> ALTIJD door. Alleen termen die in élke context OV -# betekenen (merken: NS/ProRail/Arriva; onmiskenbaar jargon: spoorstoring, -# perron, bushalte, metrostation, conducteur, ovpay; multi-woord termen). +# 1. WHITELIST -> ALTIJD door BIJ EEN HIT IN DE TITEL. Alleen termen die +# in élke context OV betekenen (merken: NS/ProRail/Arriva; onmiskenbaar jargon: +# spoorstoring, perron, bushalte, conducteur, ovpay; multi-woord termen). # REGEL: twijfel je of de term ook buiten het OV kan voorkomen -> NIET hier. -# 2. GEWOGEN FAMILIES -> score = 2x titel + 1x samenvatting >= drempel (3). +# NIET: station-samenstellingen (busstation/metrostation) — een bericht over +# een pand 'naast het busstation' is geen OV-nieuws (besluit 2026-09-11); +# die wegen mee via de gewogen families (medium). +# Een whitelist-term ALLEEN in de SAMENVATTING laat NIET onvoorwaardelijk +# door: 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 via de gewogen families (dezelfde termen staan daar ook in). +# 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). # - sterk (3): onmiskenbaar OV-jargon/merken/samenvoegingen (spoorlijn, -# treinstation, busverkeer, intercity, lijn 12, ...). +# bushalte, busverkeer, intercity, lijn 12, ...). # - medium (2): onmiskenbaar OV maar iets algemener (tram, metro, halte, -# pont, ovpay, openbaar vervoer, ...); mag niet volledig -# ontbreken, maar telt alleen mee ter ondersteuning. +# pont, ovpay, openbaar vervoer, trein-/bus-/metrostation). +# Moet COMBINEREN met een andere OV-familie: één medium-hit, +# in titel óf samenvatting, is onvoldoende — de titel weegt +# niet extra ('restaurant naast busstation' in de titel is +# geen OV-bericht, besluit 2026-09-11). +# EXTRA (besluit 2026-09-12): een medium + zwak-combinatie +# UITSLUITEND in de samenvatting (geen enkele hit in de +# titel) is geen OV-bewijs — 'openbaar vervoer' + 'zakelijke +# reizigers' in een economiebericht telt dan toch op tot de +# drempel. Sterke (3) families blijven in de samenvatting +# gewoon tellen. # - zwak (1): DUBBELZINNIGHEID. Woorden die (ook) buiten het OV # voorkomen. REGEL: elk dubbelzinnig woord krijgt een EIGEN # familie (niet samengevoegd), zodat twee verschillende @@ -129,9 +149,9 @@ MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [ # sterk (3) (r"\b(?:ns|arriva|qbuzz|connexxion|cxx|transdev|keolis|ret|gvb|ebs|htm|syntus|u-ov|allgo|flixbus|blauwnet|r-net|valleilijn|waterbus|eurostar|thalys|randstadrail|gvu|prorail|hermes|rrreis|qliner|ov)\b", 3), # vervoerders ('Breng' staat in de zwakke groep: botst met het werkwoord "brengen") (r"\b(?:spoorweg|spoorba(?:an|nen)|spoorlijn|spoorverkeer|spoorwegovergang|spoorbrug|spoorstoring|perron|seinstoring|wisselstoring|spoorstaking)\w*\b", 3), # spoorinfra; let op de klinkerwisseling: 'ontsporing' (open lettergreep, één o) vs 'ontspoord'/'ontspoorde' (dicht, dubbel o) — vandaar beide stammen - (r"\b(?:treinstation|treinstations|metrostation|metrostations|busstation|busstations)\b", 3), # station-samenstellingen: onmiskenbaar OV-jargon + (r"\b(?:treinstation|treinstations|metrostation|metrostations|busstation|busstations)\b", 2), # station-samenstellingen: onmiskenbaar OV, maar MEDIUM — éé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"(? bool: - """Gewogen familiescore: doorlaten bij 2×(titelscore) + 1×(samenvattingscore) >= drempel. + """Gewogen familiescore: elke OV-familie telt 1× per artikel (titel + samenvatting + samen; herhaling of beide velden = één signaal). Doorlaten bij som >= drempel. + (titel weegt NIET extra — één medium-hit, 'restaurant naast busstation', + is geen OV-bewijs; besluit 2026-09-11). Sterke families (gewicht 3) halen de drempel al met één woord in titel óf - samenvatting. Medium (2) in de titel ook; in de samenvatting moet er een - tweede OV-woord bijkomen. + samenvatting. Medium (2) moet combineren met een andere OV-familie. Zwakke woorden (losse 'bus', 'breng', 'overweg', 'overwegen', 'sein', 'opwaarderen', 'kaartje', 'rails', 'pendelbus') moeten combineren met een ander OV-woord én minstens één zwak woord moet in de titel staan; dezelfde - zwakke familie in titel én samenvatting is géén combinatie. Weert oa + zwakke familie in titel én samenvatting is géén combinatie. Whitelist-termen + (NS, perron, bushalte, openbaar vervoer, ...) laten alleen onvoorwaardelijk + door BIJ EEN HIT IN DE TITEL; in de samenvatting wegen ze mee als gewogen + familie. Een medium + zwak-combinatie UITSLUITEND in de samenvatting (geen + enkele hit 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 @@ -403,21 +435,33 @@ class RSSBot(Plugin): waarin de samenvatting 'station' + 'pendelbus' + 'bus' noemt. Bewust doorgelaten: krant 'De Metro'/café 'De Tram' (medium) en atleet- 'sprinter' in combinatie met ander OV-woord.""" - # Whitelist: één onmiskenbare OV-term (titel óf samenvatting) laat altijd door. - if RSSBot._whitelist_hit(title) or RSSBot._whitelist_hit(summary): + # Whitelist: een onmiskenbare OV-term in de TITEL laat altijd door (het + # artikel kondigt het onderwerp aan: 'NS verlengt noodregeling'). Een + # whitelist-term ALLEEN in de samenvatting is geen OV-bewijs ('openbaar + # vervoer' als bijzin in een economiebericht, besluit 2026-09-12) en + # scoort via de gewogen families hieronder. + if RSSBot._whitelist_hit(title): return True t_hits = RSSBot._fam_hits(title) s_hits = RSSBot._fam_hits(summary) - score = 2 * sum(w for _, w in t_hits) + sum(w for _, w in s_hits) + # 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). + combined = dict(t_hits + s_hits) + score = sum(combined.values()) if score < MATCH_FILTER_THRESHOLD: return False - if max(w for _, w in t_hits + s_hits) == 1: + if max(combined.values()) == 1: # Alleen zwakke families: dan moeten ≥2 verschillende families meetellen # én minstens één zwak woord moet in de TITEL staan. Zwakke woorden in # de samenvatting alleen zijn geen bewijs ('station' + 'pendelbus' + # 'bus' in een golfverslag telt anders op tot de drempel). - if not t_hits or len({i for i, _ in t_hits + s_hits}) < 2: + if not t_hits or len(combined) < 2: return False + elif not t_hits and 1 in combined.values(): + # Medium + 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 True @staticmethod @@ -426,18 +470,23 @@ class RSSBot(Plugin): return ( "**OV-matchfilter (vast in code, gewogen families):**\n" "- Sterk (3): vervoerders (NS, Arriva, ProRail, Qbuzz, ...), spoorinfra (spoorlijn, spoorwegovergang, perron, spoorstoring), " - "trein-/metro-/busstation, samenstellingen als bushalte/buslijn/bushokje/busbaan/busverkeer, treinstoring/treinverkeer/treinstaking, " + "samenstellingen als bushalte/buslijn/bushokje/busbaan/busverkeer, treinstoring/treinverkeer/treinstaking, " "intercity/streekvervoer/veerdienst/buurtbus/belbus, 'lijn 12'\n" - "- Medium (2): tram, metro, openbaar vervoer, lightrail, halte, pont, ov-chipkaart/ovpay, ov-staking, conducteur\n" + "- Medium (2): tram, metro, openbaar vervoer, lightrail, halte, pont, ov-chipkaart/ovpay, ov-staking, conducteur, " + "trein-/bus-/metrostation — één medium-hit volstaat niet, moet combineren met een andere OV-familie; " + "medium + 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', 'vervoerder', 'sprinter' — moet combineren met een ander OV-woord, met minstens één zwak woord in de titel\n\n" - "**Whitelist (altijd door):** één onmiskenbare OV-term in titel óf samenvatting is genoeg " - "(NS, ProRail, Nederlandse Spoorwegen, spoorstoring, perron, bushalte, metrostation, conducteur, " - "ov-chipkaart, ovpay, openbaar vervoer, ...)\n\n" + "**Whitelist (altijd door bij een hit in de TITEL):** een onmisbare OV-term in de titel is genoeg " + "(NS, ProRail, Nederlandse Spoorwegen, spoorstoring, perron, bushalte, conducteur, " + "ov-chipkaart, ovpay, openbaar vervoer, ...); in de samenvatting alleen weegt zo'n term mee " + "als gewogen familie (medium/sterk)\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: `2×(titel-score) + 1×(samenvatting-score) ≥ 3`; bij uitsluitend zwakke woorden tellen alleen " + "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" ) diff --git a/maubot.yaml b/maubot.yaml index c528086..e6a42e7 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -1,5 +1,5 @@ id: nl.eddydevink.rss -version: 1.9.6 +version: 1.10.0 modules: - buzzrss main_class: RSSBot diff --git a/validate_filter.py b/validate_filter.py index 1db6557..85a7cfb 100755 --- a/validate_filter.py +++ b/validate_filter.py @@ -90,7 +90,7 @@ for w in AMBIGUOUS: # --- Invariant 2: onmiskenbare OV-termen mogen niet uitsluitend zwak zijn UNAMBIGUOUS = [ "spoorstoring", "spoorlijn", "perron", "bushalte", "bushokje", - "metrostation", "busstation", "conducteur", "ovpay", "ns", "prorail", + "conducteur", "ovpay", "ns", "prorail", "treinstoring", "halte", "openbaar vervoer", "intercity", "buurtbus", "veerdienst", "lijn 12", "ov-chipkaart", ] @@ -139,6 +139,32 @@ GOLDEN = [ ("Bushalte aan de Markt wordt verplaatst", "De halte gaat 200 meter opschuiven.", True), ("Conducteur bekogeld in stoptrein bij Den Haag", "De conducteur is nagekeken.", True), ("Nieuwe app voor ovpay", "De ovpay-app wordt dit najaar vernieuwd.", True), + # fix 2026-09-11: station-samenstellingen zijn medium (niet meer whitelist/ + # sterk) — een enkel 'busstation' in de samenvatting naast een ander onderwerp + # ('pand naast busstation') moet GEWEERD worden, en 'busstation' telt maar 1×. + ("Restaurant weg, pand staat te verloederen: 'Doorn in het oog'", + "De gemeente Midden-Delfland wil het leegstaande horecapand naast busstation Maasland " + "Viaduct kopen. Het gebouw staat al jaren leeg en maakt een vervallen indruk.", False), + ("Pand naast busstation wordt gesloopt", "Het leegstaande pand naast het busstation gaat plat.", False), # één familie in titel ÉN samenvatting = één signaal: 2 < 3 (besluit 2026-09-11) + ("Nieuwe fietsenstalling bij busstation", "", False), # besluit 2026-09-11: één medium-hit in de titel is onvoldoende (titel weegt 1×, was 2×) + ("Restaurant naast busstation wil uitbreiden", "De eigenaar wil uitbreiden naar het naastgelegen pand.", False), # zelfde regel, letterlijk gebruikersvoorbeeld + ("Busstation wordt verbouwd", "De verbouwing duurt tot het voorjaar.", False), # herhaling/één woord = 1 signaal: 2 < 3 ('busstation' 60× weegt ook 1×) + ("Metro rijdt niet", "De halte is tijdelijk gesloten.", True), # medium-combinatie over twee families: metro(2)+halte(2)=4 + # fix 2026-09-12: whitelist-term alleen in de SAMENVATTING is geen OV-bewijs + # (valt terug op gewogen families); medium+zwak uitsluitend in de samenvatting + # (geen hit in de titel) wordt geweerd. Echte cases 11-09-2026: + # AT5-economiebericht ('openbaar vervoer' + 'zakelijke reizigers' als bijzin) + # en Telegraaf-overstroming Japan ('openbaar vervoer ontregeld' in de tekst). + ("Economische groei van de stad niet meer vanzelfsprekend: ondernemersclubs waarschuwen", + "Volgens Evers is de zakelijke bezoeker ook nodig voor het openbaar vervoer, de culturele sector en musea. " + "Het aantal zakelijke reizigers in de stad daalt.", False), + ("Straat in Japan verandert in rivier door hevige regen", + "Het verkeer ligt plat en ook het openbaar vervoer is ontregeld in de getroffen regio.", False), + # recall-bewaakt: 'openbaar vervoer' in de TITEL blijft onvoorwaardelijk door + # (whitelist-titel-anker; Havenloods 11-09-2026, 'einde aan gratis ov'). + ("Er komt echt een einde aan gratis openbaar vervoer voor ouderen", + "De gemeente stopt vanaf januari met de gratis reizen voor 65-plussers.", True), + # busstation mag niet dubbel meetellen via 'bus' én 'station' (was 2×3=6→nu 1×2) ("Metrostation Centraal tijdelijk gesloten", "De metro's rijden niet.", True), ("NS verlengt noodregeling op traject Zwolle", "Reizigers moeten rekening houden met minder treinen.", True), ("Spoorstoring bij Utrecht opgeheven", "ProRail heeft de wisselstoring verholpen.", True),