- Strip de filter terug tot: (1) sterk signaal in de titel -> door; (2) score van unieke semantische groepen >= 3 met titel-signaal -> door; (3) anders weg. Geen whitelist (82 termen), geen blacklist, geen span-merge, geen dienststoring-titelanker meer; woordgrenzen/lookbehinds weren de bekende botsingen structureel (schoolbus/VW-bus/stationsstraat/stationair/ treinstraat/treinweg). - Lijsten 49 families + 82 whitelist + 2 blacklist = 133 -> 16 sterk + 14 ambigu = 30 groepen; filterzone 524 -> ~292 regels; match-methodes 4 -> 2. - Vijf historische uitzonderings-families geschrapt (busongeluk, sein, breng, hermes, inchecken, kaartje, rails, staking, e.d.) — die woorden leveren nu 0 signalen op en de bekende false positives blijven geweerd. - Completheids-audit ving de Maaslijn/Nedersaksenlijn/Kamperlijn/waterbus- regressie op (hersteld); resterende zero-hit-kandidaten geen wijziging. - validate_filter.py: invarianten 1-3 + 113 regressie-cases (90 onveranderd groen + 23 nieuw); prompt-matrix 27/27. - Gedragswijziging op 45 productiedagen: uitsluitend false positives extra geweerd (buitenlandse veerboot-rampen, Zwitsers busongeluk, delftopzondag tram+halte, Cox/RET, tram-blokkade); 0 recalls verloren. - README/AGENTS.md/vault bijgewerkt.
229 lines
16 KiB
Python
Executable file
229 lines
16 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Blijvende filter-invarianten + regressie voor het OV-matchfilter (v1.15).
|
|
|
|
De code wordt zonder maubot-imports uit buzzrss/bot.py geëxtraheerd (AST), zodat
|
|
dit script overal los te draaien is: `python3 validate_filter.py`.
|
|
"""
|
|
import ast
|
|
import re
|
|
import sys
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
BOT = ROOT / "buzzrss" / "bot.py"
|
|
|
|
# --- extractie zonder maubot-imports (zelfde techniek als de audits) ---
|
|
src = BOT.read_text()
|
|
tree = ast.parse(src)
|
|
WANT_ASSIGN = {
|
|
"MATCH_FILTER_STRONG", "MATCH_FILTER_AMBIGUE", "MATCH_FILTER_THRESHOLD",
|
|
"_compiled_match_strong", "_compiled_match_ambigue",
|
|
}
|
|
WANT_METHODS = {"_filter_text", "_signals", "_filter_passes"}
|
|
|
|
nodes = []
|
|
for n in tree.body:
|
|
if isinstance(n, ast.Assign) and any(isinstance(t, ast.Name) and t.id in WANT_ASSIGN for t in n.targets):
|
|
nodes.append(ast.get_source_segment(src, n))
|
|
if isinstance(n, ast.AnnAssign) and getattr(n.target, "id", "") in WANT_ASSIGN:
|
|
nodes.append(ast.get_source_segment(src, n))
|
|
if isinstance(n, ast.ClassDef) and n.name == "RSSBot":
|
|
methods = []
|
|
for m in n.body:
|
|
if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef)) and m.name in WANT_METHODS:
|
|
methods.append(ast.get_source_segment(src, m))
|
|
nodes.append("class RSSBot:\n" + textwrap.indent("\n".join(methods), " "))
|
|
|
|
ns = {}
|
|
exec(compile("from __future__ import annotations\nimport re, html\n" + "\n".join(nodes) + "\n", "<bot>", "exec"), ns)
|
|
R = ns["RSSBot"]
|
|
STRONG = ns["MATCH_FILTER_STRONG"] # [(regex, naam)] — 2 punten
|
|
AMBIGUE = ns["MATCH_FILTER_AMBIGUE"] # [(regex, gewicht, naam)]
|
|
THRESHOLD = ns["MATCH_FILTER_THRESHOLD"]
|
|
COMPILED_STRONG = [(re.compile(p, re.I), n) for p, n in STRONG]
|
|
COMPILED_AMBIGUE = [(re.compile(p, re.I), w, n) for p, w, n in AMBIGUE]
|
|
|
|
failures = []
|
|
|
|
|
|
def check(cond: bool, msg: str):
|
|
print((" OK " if cond else " FOUT ") + msg)
|
|
if not cond:
|
|
failures.append(msg)
|
|
|
|
|
|
# --- Invariant 1: gewichten geldig (max-2; sterk=2, ambigu in {1,2}) ---
|
|
print("== invariant 1: gewichten -- sterk=2, ambigu in {1,2}, drempel 3 ==")
|
|
for p, w, n in AMBIGUE:
|
|
check(w in (1, 2), f"ambigu-groep {n!r} heeft geldig gewicht {w} (max-2-stelsel)")
|
|
check(THRESHOLD == 3, f"drempel = 3 (is {THRESHOLD})")
|
|
check(len(COMPILED_STRONG) == len(STRONG) and len(COMPILED_AMBIGUE) == len(AMBIGUE), "alle groepen compileren")
|
|
|
|
# --- Invariant 2: overlappingen -- een trefwoord mag hooguit door 1 groep van
|
|
# dezelfde categorie worden gematcht; sterk<->ambigu alleen voor de bewuste
|
|
# trein-groep (\w*trein\w* vangnet, dekt ook spoorjargon-woorden af).
|
|
TOKENS = [
|
|
"ns", "prorail", "arriva", "ret", "gvb", "deutsche bahn", "teso", "ov",
|
|
"ov-chipkaart", "ovpay", "spoorstoring", "spoorlijn", "spoorwegen",
|
|
"wisselstoring", "seinstoring", "ertms", "treinstoring", "treinverkeer",
|
|
"treinvertraging", "treinstel", "perron", "bushalte", "buslijn", "busbaan",
|
|
"buschauffeur", "bushokje", "busverkeer", "busstaking", "tramhalte",
|
|
"tramlijn", "tramverkeer", "metrolijn", "metroverkeer", "intercity",
|
|
"streekvervoer", "buurtbus", "belbus", "veerdienst", "conducteur",
|
|
"lightrail", "lijn 12", "rangeren", "openbaar vervoer", "ice-berlijn",
|
|
"bus", "bussen", "busstation", "busvervoer", "busongeluk", "trein",
|
|
"treinen", "stoptrein", "treinreizigers", "treinstraat", "treinweg",
|
|
"station", "stations", "stationstraat", "stationair", "stationcar",
|
|
"stationsgebouw", "metrostation", "busstation", "reiziger", "reizigers",
|
|
"passagier", "vervoer", "vervoerder", "vervoersbedrijf", "vervoerregio",
|
|
"sprinter", "pendelbus", "shuttlebus", "dienstregeling", "ontspoorde",
|
|
"ontsporing", "spoor", "spoorbrug", "halte", "haltes", "tram", "metro",
|
|
"metro's", "sprinters rijden niet", "metro rijdt niet",
|
|
]
|
|
print(f"== invariant 2: {len(TOKENS)} trefwoorden in hooguit 1 groep per categorie ==")
|
|
for tok in TOKENS:
|
|
strong_hits = [n for p, n in COMPILED_STRONG if p.search(tok)]
|
|
amb_hits = [n for p, w, n in COMPILED_AMBIGUE if p.search(tok)]
|
|
check(len(strong_hits) <= 1, f"{tok!r} in hooguit 1 sterke groep (nu: {sorted(strong_hits)})")
|
|
check(len(amb_hits) <= 1, f"{tok!r} in hooguit 1 ambigue groep (nu: {sorted(amb_hits)})")
|
|
|
|
# --- Invariant 3: semantische groepen -- varianten stapelen niet op ---
|
|
print("== invariant 3: varianten van 'bus'/'station'/'trein' = één signaal ==")
|
|
for txt, want in [
|
|
# 'bus' (1) + buslijn als sterke samenstelling (2) + busstation via station-groep (1):
|
|
# vier bus-varianten leveren géén vier onafhankelijke signalen op.
|
|
("bus buslijn busvervoer busstation", {"bussamenstelling": 2, "bus": 1, "station": 1}),
|
|
("station stationsgebouw stationsplein", {"station": 1}),
|
|
("trein treinstoring treinreizigers", {"trein": 1, "spoorjargon": 2}),
|
|
]:
|
|
got = R._signals(txt)
|
|
check(got == want, f"{txt!r} -> {got} (verwacht {want})")
|
|
|
|
# --- Regressie: gewenst gedrag (precision-first) ---
|
|
GOLDEN = [
|
|
|
|
("Na mooi seizoen in wereldstad Parijs wacht nu EK voor 'lange man' Tom Koops", 'een weekje vakantie pak ik de trein naar mijn nieuwe huis. Berlijn', False),
|
|
("Amerikaanse talkshowhost Jimmy Kimmel schrapt gesprek 'na druk regering Trump'", 'ABC, onze regionale omroepen en lokale stations.', False),
|
|
('Inchecken op Schiphol duurt langer', 'Passagiers staan in de rij bij de balies.', False),
|
|
('Sprinter verpulvert record op de 100 meter', 'De atleet liep een nationale toptijd.', False),
|
|
('Zakenreizigers vliegen weer massaal', 'Het aantal vluchten is gestegen.', False),
|
|
('Vrachtvervoerder failliet', 'De transporteur heeft uitstel van betaling aangevraagd.', False),
|
|
('Het plan voor de nieuwe wijk ontspoorde', 'De gemeente moet de kosten herzien.', False),
|
|
('Sein brandmeester: brandweer rukt uit', 'De brandweer kreeg een sein om uit te rukken.', False),
|
|
('VW-bus in brand op de A2', 'De inzittenden kwamen er veilig uit.', False),
|
|
('Fietser gewond op de Stationsstraat', 'Het slachtoffer is naar het ziekenhuis gebracht.', False),
|
|
('Kunstenaar maakt kaartje voor het museum', '', False),
|
|
('Benzineprijs stijgt opnieuw', 'De pomp wordt duurder door de olieprijs.', False),
|
|
("Golf-fans uit de hele wereld naar Cromvoirt: 'Beter dan sterrenrestaurant'", 'Op het station in Den Bosch stapte de oud-prof in de pendelbus naar Bernardus Golf. In de bus bleek ze zeker niet de enige Amerikaanse.', False),
|
|
('Toernooi trekt duizenden bezoekers', 'Vanaf het station rijden pendelbussen en extra bussen.', False),
|
|
('Trein vertraagd, reizigers pakken de bus', 'De storing duurde een uur.', True),
|
|
('Station Amersfoort krijgt nieuwe perrons', 'De NS laat de perrons vernieuwen.', True),
|
|
('Trein ontspoorde bij Weesp', 'De spoorlijn was uren dicht.', True),
|
|
('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),
|
|
("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),
|
|
('Nieuwe fietsenstalling bij busstation', '', False),
|
|
('Restaurant naast busstation wil uitbreiden', 'De eigenaar wil uitbreiden naar het naastgelegen pand.', False),
|
|
('Busstation wordt verbouwd', 'De verbouwing duurt tot het voorjaar.', False),
|
|
('Metro rijdt niet', 'De halte is tijdelijk gesloten.', True),
|
|
('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),
|
|
('Er komt echt een einde aan gratis openbaar vervoer voor ouderen', 'De gemeente stopt vanaf januari met de gratis reizen voor 65-plussers.', True),
|
|
('Metrostation Centraal tijdelijk gesloten', "De metro's rijden niet.", True),
|
|
('Frans weet wat voor warrig beestje Mony heeft gefilmd', 'Ruud Klomp zag in de Dommel bij de spoorbrug tussen Liempde en Boxtel een waterschildpad.', False),
|
|
('Spoorbrug in Boxtel wordt aangepakt', 'De werkzaamheden duren tot het najaar.', False),
|
|
('Spoorbrug wordt verhoogd voor treinen', 'De brug gaat omhoog zodat er hogere treinen onderdoor kunnen.', 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),
|
|
('Nieuwe intercity naar Berlijn', 'De trein rijdt vanaf december vier keer per dag.', True),
|
|
('Streekvervoer kampt met personeelstekort', 'Vervoerders schrappen ritten.', True),
|
|
('Buurtbus in de Achterhoek rijdt niet meer', 'Er zijn te weinig vrijwillige chauffeurs.', True),
|
|
('Veerdienst Vlissingen-Breskens vaart uit de vaart', 'De rederij zet een boot in.', True),
|
|
('Lijn 12 rijdt vanaf maandag een andere route', 'De route gaat via het centrum.', True),
|
|
('Werkzaamheden aan perron', 'Het perron wordt volgende maand opgeknapt.', True),
|
|
('Reactie Nederlandse Spoorwegen', 'De Nederlandse Spoorwegen zeggen meer in te zetten op punctualiteit.', True),
|
|
("Veel animo voor 'adopteren' graf op Amerikaanse begraafplaats Margraten", "…'Het voelde echt alsof je in de rij stond voor festivaltickets'… 'Ik ben bang dat mensen met de slaapzak voor de laptop gaan liggen', zei Ton Hermes eerder.", False),
|
|
('TESO onderzoekt komst derde veerboot tussen Texel en Den Oever', 'Veerdienst TESO onderzoekt of het haalbaar is om een derde veerboot in te zetten tussen Oudeschild en Den Oever.', True),
|
|
('Vrouw buschauffeur Rob zwaar aangeslagen na dodelijk ongeval', 'Bij het dodelijke busongeluk in Zwitserland zat de politicus achter het stuur.', True),
|
|
("Vakbonden noemen stakingsdag in het OV 'een geslaagde dag'", 'Gisteren was de landelijke stakingsdag in het OV; medewerkers legden in de spoorsector 24 uur het werk neer.', True),
|
|
('Dodental veerbootbrand Filipijnen loopt verder op', 'Zeker 76 mensen kwamen om bij de brand op de veerboot.', False),
|
|
('ICE Berlijn voegt na Hamburg vanaf december ook stop in Bremen toe', 'De ICE Berlijn gaat vanaf december via Bremen en Hamburg Hauptbahnhof naar Berlijn rijden. Dat meldt NS International.', True),
|
|
('Laatste noot voor vernielde stationspiano Eindhoven', 'De piano in het station was jarenlang een begrip reizigers produceerden er filmpjes.', False),
|
|
('Meerdere doden na busongeluk in Zwitserland', 'Het busongeluk gebeurde naast de snelweg bij een afrit.', False),
|
|
('Antwerpen ontvangt eerste ICE', 'De eerste ICE van Deutsche Bahn rijdt vanaf volgende maand naar Antwerpen.', False),
|
|
('NS verlengt noodregeling tot volgend jaar', 'De vervoerder kampt met personeelstekorten.', True),
|
|
('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),
|
|
('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),
|
|
('NS meldt storing: sprinters rijden niet', 'De storing wordt verholpen.', True),
|
|
('Metro rijdt niet tussen Rotterdam en Schiedam', "De metro's stoppen na de storing.", True),
|
|
('Buslijn 12 vervalt tijdelijk', 'De vervanging rijdt via de wijk.', True),
|
|
('Werkzaamheden aan perron', 'Het perron wordt nieuwe maand opgeknapt.', True),
|
|
('Nieuwe bushalte geopend', 'De halte ligt aan de rand van de wijk.', True),
|
|
('Sprinters rijden niet tussen Utrecht en Amsterdam', 'De oorzaak is nog onbekend.', True),
|
|
('Stoptreinen rijden niet naar Den Haag vanwege een storing', 'De NS zet bussen in.', True),
|
|
('Nieuwe woning bij station', '', False),
|
|
('Restaurant opent nieuwe vestiging', 'Het restaurant ligt tegenover het station waar dagelijks veel reizigers komen.', False),
|
|
('Stationstraat krijgt nieuwe woningen', '', False),
|
|
('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),
|
|
('Tram is een populaire naam voor het café', '', False),
|
|
('Schoolbus betrokken bij ongeval', 'De kinderen werden opgevangen.', False),
|
|
('Bus en station tijdelijk gesloten voor het feest', '', False),
|
|
('Wijkbewoners klagen over het station en de bussen', 'De gemeente komt langs.', False),
|
|
('Passagiers klagen over de incheckbalie', 'De balie opende te laat.', False),
|
|
('Gemeente verhuist naar het stationsgebouw', 'Het station en het stationsgebied worden vernieuwd.', False),
|
|
('Bus en tram krijgen in december een nieuwe dienstregeling', '', True),
|
|
('Tram krijgt nieuwe dienstregeling', 'De wijziging geldt vanaf december.', True),
|
|
('De halte wordt verplaatst voor de bus', 'De nieuwe halte komt bij de rotonde.', True),
|
|
('Metro rijdt niet vanwege een storing', 'Er is onbekend wanneer het rijden hervat.', True),
|
|
('Trein rijdt niet meer op de spoorlijn naar Den Bosch', 'Werkzaamheden duren tot maart.', True),
|
|
('Sprinters rijden niet; NS zet bussen in', '', True),
|
|
('De trams rijden niet door een storing', 'Er zijn vervangende bussen ingezet.', True),
|
|
('Tramverkeer stilgelegd na aanrijding', '', True),
|
|
('ProRail meldt wisselstoring op traject Utrecht', 'Het treinverkeer ligt stil.', True),
|
|
('Treinstoring tussen Den Haag en Leiden', 'Reizigers moeten rekening houden met vertraging.', True),
|
|
('Treinvertraging op het spoor bij Utrecht', '', True),
|
|
('NS past de dienstregeling aan', 'Door werkzaamheden rijden er minder treinen.', True),
|
|
('NS kondigt staking aan', '', True), # prompt-matrix v1.15
|
|
('ProRail meldt storing', '', True), # prompt-matrix v1.15
|
|
('Buslijn 12 gewijzigd', '', True), # prompt-matrix v1.15
|
|
('Nieuwe perrons op station', '', True), # prompt-matrix v1.15
|
|
('Metro rijdt niet vanwege storing', '', True), # prompt-matrix v1.15
|
|
('Tramverkeer stilgelegd', '', True), # prompt-matrix v1.15
|
|
('Sprinters rijden niet tussen twee steden', '', True), # prompt-matrix v1.15
|
|
('OV-chipkaart wordt vervangen', '', True), # prompt-matrix v1.15
|
|
('Nieuwe woningen bij station', '', False), # prompt-matrix v1.15
|
|
('Stationair draaiende auto', '', False), # prompt-matrix v1.15
|
|
('Hotel biedt pendelbus', '', False), # prompt-matrix v1.15
|
|
('Festival zet pendelbussen in', '', False), # prompt-matrix v1.15
|
|
('Winkel opent naast metrostation', '', False), # prompt-matrix v1.15
|
|
('Bus rijdt tegen boom', '', False), # prompt-matrix v1.15
|
|
('Trein raakt koe', '', False), # prompt-matrix v1.15
|
|
('Reizigers stranden', '', False), # prompt-matrix v1.15
|
|
('Station wordt verbouwd', '', False), # prompt-matrix v1.15
|
|
('Festival gebruikt bussen', '', False), # prompt-matrix v1.15
|
|
('Hotel heeft shuttlebus', '', False), # prompt-matrix v1.15
|
|
('Spoorbrug wordt vervangen', '', False), # prompt-matrix v1.15
|
|
("# lijnnamen (audit v1.15): onmiskenbaar OV", "", True),
|
|
("Bezwaren tegen werkzaamheden Maaslijn afgewezen - werk mag doorgaan", "", True),
|
|
("Provincie Groningen vreest voor uitstel Nedersaksenlijn", "", True),
|
|
]
|
|
print(f"== regressie: {len(GOLDEN)} cases (precision-first) ==")
|
|
for t, s, want in GOLDEN:
|
|
got = R._filter_passes(t, R._filter_text(s))
|
|
check(got == want, f"'{t[:55]}' -> {'door' if got else 'geweerd'} (verwacht {'door' if want else 'geweerd'})")
|
|
|
|
print()
|
|
if failures:
|
|
print(f"FILTER-INVALIDATIE: {len(failures)} afwijking(en) - corrigeer vóór commit/deploy.")
|
|
sys.exit(1)
|
|
print("ALLES OK - filter volgt het precision-first-beleid.")
|