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:
Eddy de Vink 2026-09-13 08:57:49 +02:00
commit 91d0c3d7fe
3 changed files with 215 additions and 141 deletions

View file

@ -25,8 +25,9 @@ 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"}
WANT_METHODS = {"_fam_hits", "_filter_passes", "_filter_text", "_whitelist_hit", "_disruption_hit"}
nodes = []
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(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)
@ -225,6 +236,46 @@ GOLDEN = [
# 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: