- _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.
289 lines
No EOL
21 KiB
Python
Executable file
289 lines
No EOL
21 KiB
Python
Executable file
#!/usr/bin/env python3
|
||
"""Blijvende filter-invarianten + regressie voor het OV-matchfilter.
|
||
|
||
Draaien (van de repo-root): python3 validate_filter.py [alleen stdlib]
|
||
|
||
Waarom dit bestaat (zie AGENTS.md):
|
||
- het classificatiebeleid (precision-first) staat als commentaar in buzzrss/bot.py;
|
||
- DIT script maakt dat beleid afdwingbaar: een latere filterwijziging die een
|
||
dubbelzinnig woord sterk/whitelist maakt, of een onmiskenbare OV-term verwakt,
|
||
faalt hierop met exit 1 — vóór commit/deploy.
|
||
"""
|
||
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_FAMILIES", "MATCH_FILTER_THRESHOLD", "MATCH_FILTER_BLACKLIST",
|
||
"MATCH_FILTER_WHITELIST", "_compiled_match_families",
|
||
"_WHITELIST_RE", "_WHITELIST_PHRASES",
|
||
"_DISRUPTION_RE_SRC", "_DISRUPTION_RE",
|
||
}
|
||
WANT_METHODS = {"_fam_hits", "_filter_passes", "_filter_text", "_whitelist_hit", "_disruption_hit"}
|
||
|
||
nodes = []
|
||
for n in tree.body:
|
||
if isinstance(n, ast.AnnAssign) and getattr(n.target, "id", "") in WANT_ASSIGN:
|
||
nodes.append(ast.unparse(n))
|
||
elif isinstance(n, ast.Assign) and any(getattr(t, "id", "") in WANT_ASSIGN for t in n.targets):
|
||
nodes.append(ast.unparse(n))
|
||
if isinstance(n, ast.ClassDef) and n.name == "RSSBot":
|
||
methods = [
|
||
ast.unparse(m)
|
||
for m in n.body
|
||
if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef)) and m.name in WANT_METHODS
|
||
]
|
||
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"]
|
||
FAMILIES = ns["MATCH_FILTER_FAMILIES"] # [(regex, gewicht)]
|
||
WHITELIST = ns["MATCH_FILTER_WHITELIST"] # frozenset
|
||
COMPILED = [(re.compile(p, re.I), w) for p, w in FAMILIES]
|
||
|
||
|
||
def weights_for(word: str) -> set[int]:
|
||
"""gewichten van families die `word` matchen (case-insensitive)."""
|
||
return {w for pat, w in COMPILED if pat.search(word)}
|
||
|
||
|
||
def whitelisted(word: str) -> bool:
|
||
return ns["_WHITELIST_RE"].search(word) is not None or any(
|
||
ph in word.lower() for ph in ns["_WHITELIST_PHRASES"]
|
||
)
|
||
|
||
|
||
failures = []
|
||
|
||
|
||
def check(cond: bool, msg: str):
|
||
print((" OK " if cond else " FOUT ") + msg)
|
||
if not cond:
|
||
failures.append(msg)
|
||
|
||
|
||
# --- Invariant 1: dubbelzinnige woorden mogen nooit sterk/medium of whitelist zijn
|
||
AMBIGUOUS = [
|
||
"trein", "bus", "station", "stations", "sprinter", "sprinters",
|
||
"reiziger", "reizigers", "passagier", "passagiers", "vervoerder", "vervoerders",
|
||
"inchecken", "uitchecken", "sein", "overweg", "overwegen", "breng",
|
||
"kaartje", "opwaarderen", "rails", "dienstregeling", "chipkaart",
|
||
"reisproduct", "vervoersbewijs", "vervoerbewijs",
|
||
"pendelbus", "shuttlebus",
|
||
"ontspor", "ontspoor", "ontspoord", "ontspoorde", "ontsporing",
|
||
# audit 2026-09-12 (max-2-stelsel + completheids-audit)
|
||
"hermes", "spoor", "staking", "stakingen", "vervoer", "vervoersbedrijf",
|
||
"vervoerregio", "ritten", "bovenleiding", "abonnement",
|
||
"busongeluk", "busongelukken", "busvervoer",
|
||
]
|
||
print(f"== invariant 1: {len(AMBIGUOUS)} dubbelzinnige woorden -> alleen zwak (1), nooit whitelist ==")
|
||
for w in AMBIGUOUS:
|
||
gew = weights_for(w)
|
||
check(1 in gew, f"{w!r} heeft een zwakke familie")
|
||
check(not (gew - {1}), f"{w!r} zit NIET in een sterke/medium familie (gewichten {sorted(gew)})")
|
||
check(not whitelisted(w), f"{w!r} staat NIET op de whitelist")
|
||
|
||
# --- Invariant 2: onmiskenbare OV-termen mogen niet uitsluitend zwak zijn
|
||
UNAMBIGUOUS = [
|
||
"spoorstoring", "spoorlijn", "perron", "bushalte", "bushokje",
|
||
"conducteur", "ovpay", "ns", "prorail",
|
||
"treinstoring", "halte", "openbaar vervoer", "intercity", "buurtbus",
|
||
"veerdienst", "lijn 12", "ov-chipkaart", "spoorbrug",
|
||
# audit 2026-09-12 (completheids-audit, max-2-stelsel)
|
||
"spoorsector", "spoorbeheerder", "spoornet", "spoorvervoerder",
|
||
"treinvertraging", "treinhinder", "treinuitval", "treinverstoring",
|
||
"ertms", "teso", "rangeerterrein", "rangeren", "ice-berlijn",
|
||
"maaslijn", "nedersaksenlijn", "kamperlijn",
|
||
"ns international", "deutsche bahn", "european sleeper", "italo", "nightjet",
|
||
]
|
||
print(f"== invariant 2: {len(UNAMBIGUOUS)} onmiskenbare OV-termen -> sterk/medium of whitelist ==")
|
||
for w in UNAMBIGUOUS:
|
||
gew = weights_for(w)
|
||
check(whitelisted(w) or any(g >= 2 for g in gew),
|
||
f"{w!r} heeft whitelist of gewicht >= 2 (gewichten {sorted(gew)})")
|
||
|
||
# --- Invariant 3: patronen compileren en gewichten geldig (max-2, besluit 2026-09-12)
|
||
print("== invariant 3: alle families compileren, gewicht in {1,2} ==")
|
||
for p, w in FAMILIES:
|
||
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")
|
||
|
||
# --- 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)
|
||
GOLDEN = [
|
||
# (titel, samenvatting, verwacht_door)
|
||
# geweerd — bekende ruis
|
||
("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),
|
||
# geweerd — zwakke woorden alléén in de samenvatting (geen titel-anker).
|
||
# Echt geval: Omroep Brabant, Solheim Cup-golfverslag (11-09-2026) — 'pendelbus'
|
||
# stond op de whitelist en 'station'+'pendelbus'+'bus' in de samenvatting was
|
||
# samen score 3.
|
||
("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),
|
||
# door — echte OV-berichten
|
||
("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),
|
||
# 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),
|
||
# fix 2026-09-12: 'spoorbrug' is medium (niet meer sterk/whitelist). Echte
|
||
# case: Omroep Brabant Stuifmail (12-09-2026) — een natuurverhaal waarin iemand
|
||
# 'bij de spoorbrug' een waterschildpad zag; dat passeerde voorheen op één
|
||
# sterke spoorbrug-hit in de samenvatting.
|
||
("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), # één medium-hit in de titel: 2 < 3
|
||
("Spoorbrug wordt verhoogd voor treinen", "De brug gaat omhoog zodat er hogere treinen onderdoor kunnen.", True), # spoorbrug(2) + trein(1) = 3 → door
|
||
("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),
|
||
# max-2-stelsel (besluit 2026-09-12): geproduceerd op 205 productie-artikelen
|
||
# + LLM-audit. Geen enkele familie weegt meer dan 2 → één hit kan nooit solo
|
||
# doorlaten; whitelist-term in de titel telt +2 (titel-anker).
|
||
("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),
|
||
# ^ echte false positive 2026-09-12: 'hermes' (achternaam) scoorde vroeger 3 via de vervoerdersfamilie
|
||
("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), # teso(2) + veerboot(2) = 4
|
||
("Vrouw buschauffeur Rob zwaar aangeslagen na dodelijk ongeval",
|
||
"Bij het dodelijke busongeluk in Zwitserland zat de politicus achter het stuur.", True), # buschauffeur(2) + busongeluk(1) = 3
|
||
("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), # ov(2) + staking(1) + spoorsector(2)
|
||
("Dodental veerbootbrand Filipijnen loopt verder op", "Zeker 76 mensen kwamen om bij de brand op de veerboot.", False), # één veerboot-hit (2) = geen NL-OV
|
||
("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), # ice-berlijn(2) + ns(2) = 4
|
||
("Laatste noot voor vernielde stationspiano Eindhoven",
|
||
"De piano in het station was jarenlang een begrip reizigers produceerden er filmpjes.", False), # cultuurstukje; 'station'/'reizigers' zwak-alleen zonder titel-anker
|
||
("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
|
||
# 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) ==")
|
||
for t, s, want in GOLDEN:
|
||
got = R._filter_passes(t, 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.") |