Compare commits
6 changed files with 191 additions and 550 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,5 +1,2 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Taakbestanden die geen onderdeel zijn van de repo
|
||||
Prompt voor agent*.md
|
||||
|
|
|
|||
|
|
@ -4,15 +4,10 @@ Maubot-plugin `nl.eddydevink.rss` (RSS → Matrix + WhatsApp), met een OV-nieuws
|
|||
|
||||
## Verplicht vóór elke filterwijziging
|
||||
1. Lees het **volledige classificatiebeleid** in `buzzrss/bot.py` — het blok BOVEN `MATCH_FILTER_FAMILIES`
|
||||
(max-2-stelsel: whitelist + gewichten 2/1, precision-first, en de werkwijze voor nieuwe woorden).
|
||||
(driedeling whitelist / sterk / medium / zwak, precision-first, en de werkwijze voor nieuwe woorden).
|
||||
2. Draai **`python3 buzzrss/validate_filter.py`** — de filter-invarianten en regressie-cases MOETEN groen
|
||||
zijn voordat filtercode gecommit/gedeployed wordt.
|
||||
3. **Compleetheids-audit (verplicht, besluit 2026-09-12):** check bij elke filterwijziging óók of de
|
||||
woordenlijsten compleet zijn — draai de OV-gap-scan over de afgewezen artikelen van de
|
||||
OV-specialist-feeds (SpoorPro.nl / Treinreiziger.nl / Mobiliteit, query op `entry` in schema `mbp_rss`),
|
||||
klasseer nieuwe kandidaten volgens het beleid (minimaal één extra LLM; enige stem TWIJFEL/NIET-OV => zwak)
|
||||
en vul de families desgevraagd aan. Exacte procedure = stap (e) in het beleidsblok in `buzzrss/bot.py`.
|
||||
4. Precision-first (gebruikersbesluit 2026-09-10): **liever een OV-bericht te veel gefilterd dan een
|
||||
3. Precision-first (gebruikersbesluit 2026-09-10): **liever een OV-bericht te veel gefilterd dan een
|
||||
niet-OV-bericht te veel doorgelaten.** Bij twijfel over een woord: zwak maken (eigen familie), nooit
|
||||
sterk/whitelist.
|
||||
|
||||
|
|
|
|||
157
README.md
157
README.md
|
|
@ -1,157 +0,0 @@
|
|||
# buzz-rss — RSS/Atom → Matrix, gefilterd op OV-relevantie
|
||||
|
||||
Maubot-plugin `nl.eddydevink.rss` (fork van [`xyz.maubot.rss`](https://github.com/maubot/rss))
|
||||
die nieuwsfeeds pollt en **alleen openbaar-vervoer-gerelateerde berichten** doorplaatst
|
||||
naar Matrix-rooms (optioneel ook alleen de link naar een WhatsApp-groep).
|
||||
|
||||
De filterlogica is **bewust vast in code** (niet via config) en volgt één businessregel:
|
||||
|
||||
> **Precision boven recall** — liever een relevant OV-artikel missen dan één
|
||||
> niet-OV-artikel ten onrechte doorlaten. (Gebruikersbesluit 2026-09-10)
|
||||
|
||||
## Functionaliteit
|
||||
|
||||
- Abonneren op feeds per room, met per-feed meldingssjabloon en `m.notice`-optie.
|
||||
- **Conditional fetch** (`ETag`/`Last-Modified`) en backoff bij faalende feeds.
|
||||
- **HTML-scrape-modus**: bronnen zonder RSS/Atom/JSON worden als pagina gepolled via
|
||||
per-feed `feed.scrape_config` (zie `buzzrss/html_scrape.py`; eerste productiefeed: CNV).
|
||||
- **Paywall-uitsluiting** (`exclude_paywalled`) en **titel-dedup over feeds**
|
||||
(`title_dedup_days`: zelfde titel van een andere site van dezelfde uitgever = niet dubbel posten).
|
||||
- Per-run logfile met aantallen (`run_log_file`).
|
||||
- **WhatsApp-forward**: alleen `entry.link` wordt gepost (vooruit-links, zonder titel),
|
||||
met dedup-check tegen de doelgroep. Uit zolang `whatsapp_api`/`whatsapp_chat_id` leeg zijn.
|
||||
|
||||
## Commando's
|
||||
|
||||
Prefix uit config (`command_prefix`, default `rss`), eventueel zonder `!`:
|
||||
|
||||
| Commando | Aliassen | Werking |
|
||||
|---|---|---|
|
||||
| `!rss subscribe <url>` | `s`, `sub` | Deze room op een feed abonneren (nieuwe feed wordt direct geïndexeerd) |
|
||||
| `!rss unsubscribe <feed-id>` | `u`, `unsub` | Abonnement van deze room opzeggen |
|
||||
| `!rss subscriptions` | `ls`, `list`, `subs` | Abonnementen in deze room + huidige filter |
|
||||
| `!rss template <feed-id> [sjabloon]` | `t`, `tpl` | Meldingssjabloon bekijken/wijzigen (variabelen: `$title`, `$link`, `$summary`, `$feed_title`, …) |
|
||||
| `!rss notice <feed-id> <true\|false>` | `n` | Als `m.notice` versturen i.p.v. `m.text` |
|
||||
| `!rss filter` | `f`, `set-filter`, `show-filter`, `get-filter` | Toont het vaste OV-matchfilter |
|
||||
|
||||
Bestuurders: admins uit de config, of gebruikers met een powerlevel ≥ dat van het
|
||||
RSS-state-event in die room (default 50 als geen level is ingesteld).
|
||||
Bij een room-tombstone worden abonnementen automatisch naar de vervangende room verplaatst.
|
||||
|
||||
## Het OV-matchfilter (v1.14.0)
|
||||
|
||||
De volledige classificatie + de werkwijze voor nieuwe woorden staat in
|
||||
`buzzrss/bot.py`, boven `MATCH_FILTER_FAMILIES` — **dat blok is leidend** bij elke
|
||||
wijziging. Samengevat:
|
||||
|
||||
```text
|
||||
score = Σ gewichten van unieke OV-termen over titel + samenvatting
|
||||
(één term = één signaal per artikel; overlappende familie-treffers wegen 1×
|
||||
met het hoogste gewicht — span-merge, besluit 2026-09-13)
|
||||
+ 2 als een whitelist-term óf een dienststoring-zin in de TITEL staat
|
||||
|
||||
doorlaten ⇔ score ≥ 3 én (titel-signaal ∨ geen zwakke term in de score)
|
||||
```
|
||||
|
||||
Stappen:
|
||||
|
||||
1. **Sterk titel-signaal — door.** Een whitelist-term (NS, ProRail, spoorstoring,
|
||||
perron, bushalte, ovpay, openbaar vervoer, …) of een dienststoring-zin
|
||||
(‹vervoerswoord› `rijdt/rijden … niet`, bv. 'sprinters rijden niet', 'metro rijdt niet')
|
||||
in de titel is voldoende.
|
||||
2. **Context — som ≥ 3.** Gewichten: **sterk (2)** = onmiskenbaar OV
|
||||
(vervoerders, spoorinfra, bus-samenstellingen, trein-storingsjargon, tram/metro/halte,
|
||||
storingszin, …), **zwak (1)** = dubbelzinnig, elk met een eigen familie
|
||||
(trein, bus, station(s), sprinter, reiziger, vervoerder, sein, overweg, breng, …)
|
||||
en alleen als combinatie geldig. Max-2-stelsel: geen enkele familie weegt meer dan 2,
|
||||
één sterke hit kan nooit solo doorlaten.
|
||||
3. **Titel-anker.** Zonder enig OV-signaal in de titel (geen familie-hit, geen
|
||||
whitelist-term, geen storingszin) mag de score geen zwak woord bevatten —
|
||||
'station' + 'pendelbus' + 'bus' in een samenvatting is geen OV-bewijs.
|
||||
Uitzondering die bewust blijft: **twee sterke termen in de samenvatting** (bv. de
|
||||
delftopzondag-bereikbaarheidszin 'tram 1, halte Buitenhofdreef', besluit 2026-09-13).
|
||||
|
||||
Zwarte lijst (`MATCH_FILTER_BLACKLIST`): gematchte woorden zonder OV-waarde —
|
||||
na de v1.14.0-opschoning alleen nog `treinstraat`/`treinweg`. De oudere
|
||||
stations\*-vormen (`stationsstraat`, `stationair`, `stationcar`, `stationeren`, …)
|
||||
zijn inert sinds de woordgrens-familie `\bstations?\b` en worden door tests bewaakt.
|
||||
|
||||
### Bekende bewust-geaccepteerde doorlatingen / weigeringen
|
||||
|
||||
- *Door*: 'met een tram en halte bereikbaar'-zinnen in lokale berichten (2 sterke
|
||||
termen, samenvatting) — kurk op de fles is het max-2-stelsel + titel-anker.
|
||||
- *Geweerd*: krant 'De Metro', café 'De Tram', atleet-'sprinter',
|
||||
'VW-bus'/'schoolbus' (koppelteken- en woordgrensregels), werkwoord 'brengen',
|
||||
idioom 'goed met elkaar overweg kunnen', 'een wijk opwaarderen',
|
||||
brandweerjargon 'sein brandmeester', figuratief 'weer op de rails',
|
||||
achternaam 'Hermes' (Margraten), 'restaurant/woning naast busstation'.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
python3 validate_filter.py
|
||||
```
|
||||
|
||||
Vaste invarianten + regressie (momenteel **90 cases**):
|
||||
|
||||
1. dubbelzinnige woorden → alleen zwak (1), nooit whitelist;
|
||||
2. onmiskenbare OV-termen → sterk (2) of whitelist;
|
||||
3. alle families compileren, gewichten in {1, 2};
|
||||
4. elke whitelist-term zit óók in een familie én elke zwartgelijste term wordt door
|
||||
een familie gematcht (geen inert gewicht).
|
||||
|
||||
**Verplicht vóór elke filterwijziging** (zie `AGENTS.md`): het classificatiebeleid in
|
||||
`buzzrss/bot.py` lezen, `validate_filter.py` groen, en de **compleetheids-audit**:
|
||||
OV-gap-scan over de afgewezen artikelen van de specialist-feeds (SpoorPro,
|
||||
Treinreiziger, Mobiliteit; query op `entry` in PostgreSQL-schema `mbp_rss`),
|
||||
nieuwe woordkandidaten klasseer je volgens het beleid (minimaal één extra
|
||||
taalmodel; enige stem 'TWIJFEL'/'NIET-OV' ⇒ zwak).
|
||||
|
||||
## Configuratie
|
||||
|
||||
Zie `base-config.yaml` (wordt in het gebouwde `.mbp` meegeleverd):
|
||||
|
||||
| Sleutel | Default | Werking |
|
||||
|---|---|---|
|
||||
| `update_interval` | 60 | Poll-interval in minuten |
|
||||
| `max_backoff` | 7200 | Maximale backoff (minuten) bij faalende feeds |
|
||||
| `poll_parallelism_limit` | 10 | Maximaal aantal feeds tegelijk pollend |
|
||||
| `spam_sleep` | 2 | Pauze tussen berichten bij broadcast (‑1 = parallel) |
|
||||
| `command_prefix` | `rss` | Commando-prefix |
|
||||
| `notification_template` | `New post in $feed_title: [$title]($link)` | Standaard meldingssjabloon |
|
||||
| `run_log_file` | `/data/buzzrss-runs.log` | Per-run samenvattingsregel (leeg = uit) |
|
||||
| `whatsapp_api` / `whatsapp_session` / `whatsapp_chat_id` | – | WhatsApp-forward (leeg adapter = uit) |
|
||||
| `exclude_paywalled` | true | Betaalmuur-artikelen overslaan |
|
||||
| `title_dedup_days` | 30 | Cross-feed titel-dedup-venster (0 = uit) |
|
||||
| `admins` | – | Gebruikers die room-permissies mogen omzeilen |
|
||||
|
||||
## Deploy
|
||||
|
||||
Productie draait het **geïnstalleerde `.mbp`** (maubot), niet deze werkdirectory —
|
||||
een commit is pas actief na build + upload + restart. Exacte recepten staan in de
|
||||
maubot-beheerskill (`skill://maubot`) en de Obsidian-vault (Matrix Synapse en MAS.md).
|
||||
|
||||
In het kort:
|
||||
|
||||
```text
|
||||
1. mbc build uit de VOLLEDIGE bron (buzzrss/ + maubot.yaml + base-config.yaml —
|
||||
zonder base-config.yaml faalt de instance met BaseMissingError)
|
||||
2. POST /_matrix/maubot/v1/plugins/upload?allow_override=true (admin-login)
|
||||
3. docker restart matrix-bot-maubot
|
||||
```
|
||||
|
||||
**NOOIT maubot `reload` gebruiken** — die laat zombie-commandhandlers achter
|
||||
(één commando → N antwoorden). Alleen een proces-herstart wist de handlers.
|
||||
|
||||
## Architectuur in het kort
|
||||
|
||||
- Poll-loop → per feed conditional-fetch of HTML-scrape → nieuwe entries
|
||||
(`feed_id` + `id` PK-dedup) → cross-feed titel-dedup → `_filter_passes` →
|
||||
broadcast naar alle subscriptions + optioneel WhatsApp (`_send_whatsapp`).
|
||||
- Persistente tabel `whatsapp_posted` voor WhatsApp-dedup.
|
||||
- DB: PostgreSQL-schema `mbp_rss` (`feed`, `entry`, `subscription`,
|
||||
`whatsapp_posted`); migraties via maubot's `UpgradeTable` (`buzzrss/migrations.py`).
|
||||
|
||||
## Licentie
|
||||
|
||||
AGPL-3.0-or-later (upstream: maubot/rss, © Tulir Asokan).
|
||||
463
buzzrss/bot.py
463
buzzrss/bot.py
|
|
@ -55,28 +55,23 @@ 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). Eén term die meerdere families matcht telt óók maar 1× (hoogste
|
||||
# gewicht wint): 'ov-chipkaart' bevat 'chipkaart', 'openbaar vervoer' bevat
|
||||
# 'vervoer', 'treinstoring' matcht de zwakke \w*trein\w*-familie mee — geen
|
||||
# dubbeltelling (besluit 2026-09-13; 'busbaan' stond in twee families en is
|
||||
# uit de halte-familie verwijderd, refactor 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).
|
||||
# Gewicht 2 (onmiskenbaar OV: merken, sectorjargon, samenstellingen als bushalte/
|
||||
# spoorlijn/veerboot — de vroegere 'sterk(3)' én 'medium(2)' zijn hierin
|
||||
# opgegaan, want beide moeten nu combineren).
|
||||
# Gewicht 1 (zwak/dubbelzinnig): moet met een andere OV-familie combineren
|
||||
# (een tweede, andere familie — dezelfde zwakke familie in titel én samenvatting
|
||||
# telt niet als combinatie) EN minstens één zwak woord moet in de TITEL staan:
|
||||
# een combinatie van alleen zwakke woorden in de samenvatting is geen bewijs
|
||||
# (het golfbericht met "station" + "pendelbus" + "bus" in de samenvatting is
|
||||
# geen OV-nieuws). Weert zo "VW-bus"/"schoolbus", "weer op de rails", het
|
||||
# werkwoord "brengen", het idioom "goed met elkaar overweg kunnen", het werkwoord
|
||||
# "overwegen", het werkwoord "opwaarderen" en brandweerjargon ("sein brandmeester"),
|
||||
# zonder eenduidige bushalte/overweg/NS/trein-berichten te raken.
|
||||
# 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): 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
|
||||
# samenvatting telt niet als combinatie) EN minstens één zwak woord moet in de
|
||||
# TITEL staan: een combinatie van alleen zwakke woorden in de samenvatting is
|
||||
# geen bewijs (het golfbericht met "station" + "pendelbus" + "bus" in de
|
||||
# samenvatting is geen OV-nieuws). Weert zo "VW-bus"/"schoolbus", "weer op de rails", het
|
||||
# werkwoord "brengen" ("hoe breng je ... in beeld"), het idioom "goed met elkaar
|
||||
# overweg kunnen", het werkwoord "overwegen" ("gratieverlening overwegen"), het
|
||||
# werkwoord "opwaarderen" ("een wijk opwaarderen") en brandweerjargon
|
||||
# ("sein brandmeester"), zonder eenduidige bushalte/overweg/NS/trein-berichten
|
||||
# te raken.
|
||||
# Inflectie: families matchen met \w* aan het eind, zodat meervoud en vervoeging
|
||||
# meegenomen worden ("spoorlijnen", "seinstoringen", "ontspoorde", "chipkaarten").
|
||||
# Een koppelteken vóór een match is géén OV-signaal: "VW-bus" en "radio-station"
|
||||
|
|
@ -85,148 +80,105 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
|
|||
# straatnamen rond stations — "de Stationsstraat afsluiten voor een festival"
|
||||
# is geen OV-nieuws). Zwartgelijste woorden leveren geen score op, maar
|
||||
# blokkeren de familie niet: een écht "station"-woord ernaast scoort gewoon.
|
||||
# Bewust geaccepteerde botsingen: krant "De Metro"/café "De Tram" (gewicht 2,
|
||||
# moet combineren). 'spoorbrug' weegt 2: een brug is vaak louter locatie
|
||||
# ('bij de spoorbrug tussen Liempde en Boxtel', Stuifmail 2026-09-12) — écht
|
||||
# spoorbrug-nieuws combineert met trein/spoor/verkeer (besluit 2026-09-12).
|
||||
# Naam-botsingen die bewust zijn afgezwakt (2026-09-12): 'hermes' (vervoerder
|
||||
# Hermes vs. achternaam Ton Hermes — Margraten-false positive), 'breng'
|
||||
# (vervoerder vs. werkwoord 'brengen'), 'spoor' (figuurlijk 'op het spoor van').
|
||||
# Losse algemene woorden zijn zwak (moeten combineren): "trein", "sprinter"
|
||||
# (atleet), "vervoerder" (logistiek), "reiziger"/"passagier" (vliegen),
|
||||
# Bewust geaccepteerde botsingen: krant "De Metro"/café "De Tram" (medium).
|
||||
# 'spoorbrug' is MEDIUM (niet sterk/whitelist): een brug is vaak louter locatie
|
||||
# ('bij de spoorbrug tussen Liempde en Boxtel', Stuifmail 2026-09-12) — één
|
||||
# spoorbrug-hit in de samenvatting is geen OV-nieuws; écht spoorbrug-nieuws
|
||||
# combineert met trein/spoor/verkeer en haalt de drempel alsnog (besluit 2026-09-12).
|
||||
# Losse algemene woorden zijn zwak (moeten combineren met een andere familie):
|
||||
# "trein", "sprinter" (atleet), "vervoerder" (logistiek), "reiziger",
|
||||
# "inchecken"/"uitchecken" (Schiphol/hotel), "station(s)" (omroepen), "bus",
|
||||
# "sein" (brandweer), "overweg", "breng", "kaartje", "opwaarderen", "rails",
|
||||
# "staking" (alle sectoren), "vervoer" (vracht/auto), "abonnement", "ritten".
|
||||
# Multi-woord-frasen en merknamen (ice-berlijn, deutsche bahn, european sleeper,
|
||||
# ns international, teso, italo, nightjet, maaslijn) zijn onmiskenbaar OV en
|
||||
# wegen 2; een los 'ice' is NIET toevoegbaar (case-insensitive botsing met 'ijs',
|
||||
# besluit 2026-09-12).
|
||||
# "sein" (brandweer), "overweg", "breng", "kaartje", "opwaarderen", "rails".
|
||||
# =====================================================================
|
||||
# FILTERCLASSIFICATIE-BELEID (bij elke aanpassing aan het filter volgen)
|
||||
# =====================================================================
|
||||
# Doel: precisie boven recall. Liever een OV-bericht te veel gefilterd dan
|
||||
# een niet-OV-bericht te veel doorgelaten (gebruikersbesluit, 2026-09-10).
|
||||
#
|
||||
# Beslissing, in drie stappen (refactor 2026-09-13; logica equivalent met de
|
||||
# voorheen gelaagde takken — MAX-2-stelsel, besluit 2026-09-12):
|
||||
# 1. STERK SIGNAAL (whitelist-term óf dienststoring-zin in de TITEL) -> DOORLATEN.
|
||||
# Whitelist = termen die in élke context OV betekenen (merken:
|
||||
# NS/ProRail/Arriva; onmiskenbaar jargon: spoorstoring, perron, bushalte,
|
||||
# conducteur, ovpay; multi-woord-frasen: nederlandse spoorwegen, openbaar
|
||||
# vervoer, ice-berlijn ...). Een whitelist-hit in de titel telt +2,
|
||||
# bovenop het familie-gewicht van dezelfde term (elke whitelist-term zit
|
||||
# óók in een familie) — samen altijd >= drempel, dus eenduidige OV-titels
|
||||
# ('Reactie Nederlandse Spoorwegen', 'Werkzaamheden aan perron') passeren
|
||||
# alleen. REGEL: twijfel je of de term ook buiten het OV kan voorkomen ->
|
||||
# NIET hier, maar zwak (eigen familie). Een whitelist-term ALLEEN in de
|
||||
# SAMENVATTING blijft inert: 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 alleen via de familie.
|
||||
# 2. CONTEXT: som van unieke OV-signalen over titel + samenvatting
|
||||
# >= drempel (3). Per familie 1× per artikel (herhaling of beide velden
|
||||
# tellen niet dubbel, besluit 2026-09-11); overlappende familie-treffers
|
||||
# van dezelfde term wegen 1× met het hoogste gewicht (span-merge in
|
||||
# _fam_hits, besluit 2026-09-13 — 'ov-chipkaart' bevat 'chipkaart' maar
|
||||
# weegt als één term 2, geen 3).
|
||||
# - sterk (2): onmiskenbaar OV-jargon/merken/samenvoegingen (spoorlijn,
|
||||
# bushalte, busverkeer, intercity, veerboot, ertms,
|
||||
# rangeer..., ice-frasen, ...) én het dienststoring-
|
||||
# contextpatroon 'vervoerswoord + rijdt/rijden (niet) +
|
||||
# niet' ('sprinters rijden niet', 'metro rijdt niet').
|
||||
# Eén hit (titel óf samenvatting) is onvoldoende: moet
|
||||
# combineren (2 + 1 = 3). De vroegere 'één sterk woord
|
||||
# volstaat'-regel is vervallen — 'hermes' als achternaam
|
||||
# passeerde zo op één vervoerder-hit (Margraten,
|
||||
# 2026-09-12).
|
||||
# Drie lagen, elk met een eigen regel:
|
||||
# 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.
|
||||
# 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,
|
||||
# bushalte, busverkeer, intercity, lijn 12, ...).
|
||||
# - medium (2): onmiskenbaar OV maar iets algemener (tram, metro, halte,
|
||||
# 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
|
||||
# zwakke woorden wél als combinatie tellen, maar één enkel
|
||||
# woord nooit de doorslag kan geven. Bekende dubbelzinnige:
|
||||
# trein, bus, station(s), sprinter, reiziger/passagier,
|
||||
# vervoerder, inchecken/uitchecken, sein, overweg, breng,
|
||||
# kaartje, opwaarderen, rails, dienstregeling, chipkaart,
|
||||
# reisproduct, vervoer(s)bewijs, ontsporen,
|
||||
# trein, bus, station(s), sprinter, reiziger, vervoerder,
|
||||
# inchecken/uitchecken, sein, overweg, breng, kaartje,
|
||||
# opwaarderen, rails, dienstregeling, chipkaart, ontsporen,
|
||||
# pendelbus/shuttlebus (evenement-/hotel-/parkeerpendel;
|
||||
# synoniemen -> bewust één familie), busongeluk/busvervoer
|
||||
# (touringcar/schoolbus), staking, vervoer, vervoersbedrijf,
|
||||
# vervoerregio, ritten, bovenleiding (industrie),
|
||||
# abonnement, hermes, spoor.
|
||||
# 3. TITEL-ANKER: staat er geen enkel OV-signaal in de titel (geen
|
||||
# familie-hit, geen whitelist-term), dan is de score uitsluitend
|
||||
# samenvatting-bewijs en mag die geen zwak woord bevatten. Twee/meer
|
||||
# zwakke woorden in de samenvatting ('station' + 'pendelbus' + 'bus' in
|
||||
# een golfverslag) en sterk+zwak uitsluitend in de samenvatting
|
||||
# ('openbaar vervoer' + 'zakelijke reizigers' in een economiebericht,
|
||||
# beide besluiten 2026-09-12) zijn geen OV-bewijs. Alleen twee sterke
|
||||
# termen in de samenvatting blijven bewust door (delftopzondag
|
||||
# 'tram 1, halte Buitenhofdreef', besluit 2026-09-13).
|
||||
# BLACKLIST -> gematchte woorden zonder OV-waarde, restant van
|
||||
# straatnamen rond spoorlijnen die de zwakke \b\w*trein\w*\b-familie zou
|
||||
# matchen ('treinstraat'/'treinweg'); blokkeren de familie niet. De
|
||||
# oudere stations*-varianten zijn sinds de woordgrens-familie
|
||||
# \bstations?\b inert en verwijderd (2026-09-13) — gedrag vastgelegd in
|
||||
# validate_filter.py.
|
||||
# synoniemen -> bewust één familie, anders tellen twee
|
||||
# synoniemen als twee onafhankelijke signalen).
|
||||
# EXTRA REGEL voor zwak-alleen: staat er uitsluitend zwak
|
||||
# bewijs in de SAMENVATTING (geen enkele zwakke hit in de
|
||||
# titel), dan niet doorlaten. Twee/meer zwakke woorden in
|
||||
# de samenvatting zijn anders samen al 3 punten
|
||||
# ("station" + "pendelbus" + "bus" in een golfverslag).
|
||||
# 3. BLACKLIST -> gematchte woorden zonder OV-waarde (stationsstraat,
|
||||
# stationair, stationcar, ...); blokkeren de familie niet.
|
||||
#
|
||||
# BEOORDELEN VAN EEN NIEUW WOORD (incl. COMPLETEHEIDS-AUDIT, 2026-09-12):
|
||||
# BEOORDELEN VAN EEN NIEUW WOORD:
|
||||
# a) Kan het ook buiten het OV voorkomen? ja -> zwak (1), eigen familie.
|
||||
# b) Is het een samenvoeging/merk die alleen OV kan betekenen? -> sterk (2)
|
||||
# of whitelist (frasen die in élke context OV betekenen).
|
||||
# b) Is het een samenvoeging/merk die alleen OV kan betekenen? -> sterk (3)
|
||||
# of whitelist.
|
||||
# c) Bij twijfel: zwak (1). Conservatief = precision-first.
|
||||
# d) Ter controle: leg nieuwe woordkandidaten voor aan minimaal één extra
|
||||
# taalmodel (de audit op 2026-09-10 gebruikte 3 modellen; enige stem
|
||||
# 'TWIJFEL'/'NIET-OV' => behandelen als dubbelzinnig).
|
||||
# e) COMPLETEHEID (besluit 2026-09-12): check bij elke filterwijziging óók of
|
||||
# de families compleet zijn — draai de OV-gap-scan over de afgewezen
|
||||
# artikelen van OV-specialist-feeds (SpoorPro/Treinreiziger/Mobiliteit),
|
||||
# classificeer nieuwe kandidaten via stap a-d en vul aan. De audit van
|
||||
# 2026-09-12 leverde o.a. spoorsector, spoorbeheerder, spoornet,
|
||||
# spoorvervoerder, treinvertraging/uitval, ertms, teso, rangeer, ice-frasen,
|
||||
# maaslijn, ns international, deutsche bahn, european sleeper, italo,
|
||||
# nightjet (sterk) en 11 zwakke woorden op.
|
||||
# Dienststoring-contextpatroon: vervoerswoord + 'rijdt/rijden (niet)' + 'niet'
|
||||
# ('sprinters rijden niet', 'metro rijdt niet tussen Rotterdam en Schiedam').
|
||||
# Gedeeld door de familie én het titel-anker (_disruption_hit): in de TITEL is de
|
||||
# storingszin een sterk OV-signaal (net als een whitelist-term), in de samenvatting
|
||||
# weegt hij als familie (2). 'rijden' an sich is géén OV-signaal. 'schoolbus'
|
||||
# matcht niet ('bus' niet aan het woordbegin), refactor 2026-09-13.
|
||||
_DISRUPTION_RE_SRC = (
|
||||
r"\b(?:sprinters?|stoptrein(?:en)?|intercity'?s?|trein(?:en)?|trams?|bussen|bus|"
|
||||
r"metro'?s?|veerdienst(?:en)?)\s+(?:rijdt|rijden|reed|reden)\s+niet\b"
|
||||
)
|
||||
|
||||
# =====================================================================
|
||||
MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
|
||||
# sterk (2) — onmiskenbaar OV, maar één hit is onvoldoende (max-2-stelsel,
|
||||
# besluit 2026-09-12): elke echte OV-bericht combineert met een tweede signaal.
|
||||
(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|rrreis|qliner|ov|teso|deutsche bahn|european sleeper|ns international|italo|nightjet)\b", 2), # vervoerders; 'hermes' is zwak (achternaam-botsing, Margraten 2026-09-12), 'Breng' ook (werkwoord "brengen"); internationale vervoerders: teso (veer), deutsche bahn/european sleeper/italo/nightjet/ns international (audit 2026-09-12)
|
||||
(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
|
||||
# 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|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. Géén 'spoorbrug': die is medium (vaak locatie-referentie, besluit 2026-09-12)
|
||||
(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"(?<![\w-])\bstations?\b", 1), # los 'station(s)' is ZWAK: ook tv-/radio-/regionale omroepstations ('de lokale stations'), moet combineren met een andere OV-familie
|
||||
(r"\b(?:bus|tram|metro)-?(?:halte|haltes|lijn|lijnen|verkeer|net|staking|ba(?:an|nen)|hok(?:je|jes)|chauffeur)\w*\b", 2), # voertuig-samenstelling (bushalte/buslijn/bushokje/busbanen/buschauffeur/…); géén 'station': *station-samenstellingen zitten in de station-familie, anders telde 'busstation' dubbel mee — fix 2026-09-11. Géén 'ongeluk'/'vervoer': die zijn bewust zwak (busongeluk = ook touringcar, busvervoer = ook bedrijfsvervoer, audit 2026-09-12). 'busbaan'/'bushokje' stonden óók in de halte-familie — verwijderd bij de refactor 2026-09-13 (geen dubbelwoorden in twee families)
|
||||
(r"\b(?:treinstoring|treinverkeer|treinstaking|treinongeval|treinbotsing|treinongeluk|treinvertraging|treinhinder|treinuitval|treinverstoring)\w*\b", 2), # trein-jargon-samenstellingen: onmiskenbaar OV; uitgebreid met vertraging/hinder/uitval/verstoring (audit 2026-09-12)
|
||||
(r"\b(?:bus|tram|metro)-?(?:halte|haltes|lijn|lijnen|verkeer|net|staking|ba(?:an|nen)|hok(?:je|jes)|chauffeur)\w*\b", 3), # voertuig-samenstelling (bushalte/buslijn/bushokje/busbanen/…); géén 'station': *station-samenstellingen zitten in de station-familie, anders telde 'busstation' dubbel mee (via 'bus' én 'station') — fix 2026-09-11
|
||||
(r"\b(?:treinstoring|treinverkeer|treinstaking|treinongeval|treinbotsing|treinongeluk)\w*\b", 3), # trein-jargon-samenstellingen: onmiskenbaar OV
|
||||
(r"\b\w*trein\w*\b", 1), # losse 'trein' en algemene samenstellingen (stoptrein, goederentrein, treinverkeer al in bovenstaande familie) — ZWAK: 'de trein naar mijn nieuwe huis' is geen OV-jargon, moet combineren met een andere familie
|
||||
(r"\b(?:intercity|streekvervoer|stadsvervoer|veerdienst|veerboot|veerboten|veerpont|voetveer|veerverbinding|buurtbus|belbus|maaslijn|nedersaksenlijn|kamperlijn)\w*\b", 2), # treintypes, vervoerssectoren, veren en lijnnamen ('sprinter' staat in de zwakke groep: atleet-botsing); maaslijn/nedersaksenlijn/kamperlijn via audit 2026-09-12
|
||||
(r"\blijn\s?\d{1,3}\b", 2), # 'lijn 12' — vervoerslijn met nummer
|
||||
(r"(?:\brangeer\w*|\brangeren\w*)\b", 2), # rangeren/rangerend/rangeerterrein/rangeerbedrijf: onmiskenbaar spoorjargon (audit 2026-09-12; stam 'rangeren' = ranger+en, 'rangeerterrein' = rangeer+...)
|
||||
(r"\bice[\s\-–—]*(?:berlijn|hamburg|brussel|bremen|parijs|frankfurt|keulen|amsterdam|bazel|milaan|kopenhagen|zurich|zürich|duitsland|international)\b|\bice-trein\w*\b", 2), # ICE-frasen: onmiskenbaar trein; los 'ice' is NIET toevoegbaar (case-insensitive botsing met 'ijs', besluit 2026-09-12)
|
||||
# onmiskenbaar OV maar iets algemener (2, moet ook combineren)
|
||||
(r"\b(?:intercity|streekvervoer|stadsvervoer|veerdienst|veerboot|veerboten|veerpont|voetveer|veerverbinding|buurtbus|belbus)\w*\b", 3), # treintypes, vervoerssectoren en veren ('sprinter' staat in de zwakke groep: atleet-botsing)
|
||||
(r"\blijn\s?\d{1,3}\b", 3), # 'lijn 12' — vervoerslijn met nummer
|
||||
# medium (2)
|
||||
(r"\btrams?\b", 2),
|
||||
(r"\bmetro(?:'?s)?\b", 2),
|
||||
(r"\b(?:openbaar vervoer|lightrail|halte)\w*\b", 2), # frasen en halte-hits; 'bushokje'/'busbaan' matchen al de bus-samenstelling-familie — bewust niet dubbel in twee families (refactor 2026-09-13)
|
||||
(r"\b(?:openbaar vervoer|lightrail|halte|bushokje|busbaan)\w*\b", 2),
|
||||
(r"\b(?:ov-chipkaart|ovpay|ov-pay|studentenreisproduct)\w*\b", 2),
|
||||
(r"\b(?:ov-staking|conducteursstaking)\w*\b", 2),
|
||||
(_DISRUPTION_RE_SRC, 2), # dienststoring-context ('sprinters rijden niet', 'metro rijdt niet...'); zie _DISRUPTION_RE_SRC
|
||||
(r"\bconduct(?:eur|eurs)\b", 2), # trein-/buspersoneel: onmiskenbaar OV-jargon
|
||||
(r"\b(?:pont|ponten)\b", 2), # waterbus/veerdienst
|
||||
(r"\bspoorbrug(?:gen)?\b", 2), # spoorinfra, maar vaak locatie-referentie ('bij de spoorbrug', Stuifmail 2026-09-12); moet combineren — zelfde regel als station-samenstellingen
|
||||
(r"\bspoorbrug(?:gen)?\b", 2), # spoorinfra, maar MEDIUM: vaak locatie-referentie, geen OV-nieuws ('bij de spoorbrug', Stuifmail 2026-09-12); moet combineren — zelfde regel als station-samenstellingen (besluit 2026-09-11)
|
||||
# zwak (1) — dubbelzinnig, moet combineren
|
||||
(r"(?<!-)\bbus(?:sen)?\b", 1), # ná een koppelteken geen OV: 'VW-bus', 'school-bus'
|
||||
(r"\b(?:pendelbus|shuttlebus)\w*\b", 1), # evenement-/hotel-/parkeerpendel ('pendelbus naar de golfbaan') is geen OV; synoniemen bewust in één familie
|
||||
(r"\b(?:inchecken|uitchecken)\w*\b", 1), # ook op luchthavens/hotels — geen OV-jargon, moet combineren
|
||||
(r"\bsprinters?\b", 1), # ook atleet ('sprinter verpulvert record') — aparte familie, moet combineren
|
||||
(r"\bvervoerders?\b", 1), # ook logistiek ('vrachtvervoerder') — aparte familie, moet combineren
|
||||
(r"\b(?:reizigers?|passagiers?)\b", 1), # ook 'zakenreizigers vliegen' / vliegtuigpassagiers — synoniemen bewust één familie (audit 2026-09-12)
|
||||
(r"\breizigers?\b", 1), # ook algemeen ('zakenreizigers vliegen') — aparte familie, moet combineren
|
||||
(r"\bdienstregeling\w*\b", 1), # dubbelzinnig (modelaudit) — meestal OV, maar moet combineren
|
||||
(r"\bchipkaart(?:en)?\b", 1), # dubbelzinnig (bankpas/algemeen) — aparte familie
|
||||
(r"\breisproduct(?:en)?\b", 1), # dubbelzinnig — aparte familie
|
||||
|
|
@ -239,73 +191,59 @@ MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
|
|||
(r"\bkaart(?:je|jes)\b", 1),
|
||||
(r"\bopwaarderen\b", 1), # ov-chipkaart opladen vs werkwoord "een wijk/park opwaarderen"
|
||||
(r"\brails\b", 1),
|
||||
(r"\bbusongeluk(?:ken)?\b", 1), # audit 2026-09-12: ook touringcar/schoolbus; combineert met buschauffeur/bus → door (busongeluk Zwitserland)
|
||||
(r"\bbusvervoer\b", 1), # audit 2026-09-12: ook touringcar-/bedrijfsvervoer
|
||||
(r"\bspoor\b", 1), # figuurlijk 'op het spoor van de daders', 'het spoor bijster'; écht spoornieuws combineert met spoorlijn/trein/perron
|
||||
(r"\bstaking(?:en)?\b", 1), # arbeidsstaking in alle sectoren; 'spoorstaking'/'busstaking'/'ov-staking' zitten in de sterke families (besluit 2026-09-12)
|
||||
(r"\bvervoer\b", 1), # ook vracht-/autovervoer; 'openbaar vervoer' zit in de sterke familie; samenstellingen (streekvervoer e.d.) matchen hier niet (geen \b)
|
||||
(r"\bvervoersbedrijf(?:ven)?\b", 1), # ook goederen-/taxibedrijf (audit 2026-09-12)
|
||||
(r"\bvervoerregio'?s?\b", 1), # OV-autoriteit ('Vervoerregio Amsterdam'), maar audit: ook algemener vervoersgebied
|
||||
(r"\britten\b", 1), # ook fiets-/taxiritten (audit 2026-09-12)
|
||||
(r"\bbovenleiding(?:en)?\b", 1), # audit 2026-09-12: ook water/gas/elektra-bovenleiding; spoor-combinatie (spoorlijn/ertms) haalt de drempel
|
||||
(r"\babonnement(?:en)?\b", 1), # gym/museum/telefoon (audit 2026-09-12)
|
||||
(r"\bhermes\b", 1), # vervoerder Hermes vs. achternaam (Ton Hermes, stichting Adoptie Graven Margraten, 2026-09-12)
|
||||
]
|
||||
|
||||
# Drempel voor doorlaten: som van unieke familie-gewichten >= 3. Max-2-stelsel
|
||||
# (besluit 2026-09-12): geen enkele familie weegt meer dan 2, dus een enkele hit
|
||||
# kan nooit solo doorlaten; een whitelist-term in de titel telt +2 (zie
|
||||
# _filter_passes). De oude '2×titel + 1×samenvatting'-rekensom is vervallen
|
||||
# (2026-09-11: de titel weegt niet extra).
|
||||
# Drempel voor doorlaten (score = 2×titel + 1×samenvatting).
|
||||
MATCH_FILTER_THRESHOLD = 3
|
||||
|
||||
# Woorden die een familie wél matcht maar die geen OV-waarde hebben. Een hit dat
|
||||
# hierin staat telt niet mee voor de score (en blokkeert de familie niet). Vorm:
|
||||
# exact woord (lowercase), zoals door de familie-regex wordt gematcht.
|
||||
# Refactor 2026-09-13: alleen woorden die een HUIDIGE familie nog matchen blijven
|
||||
# staan ('treinstraat'/'treinweg' via de zwakke \b\w*trein\w*\b-familie). De
|
||||
# stations*-varianten (stationsstraat, stationair, stationeren, stationcar,
|
||||
# stationwagen, stationsplein, ...) waren inert sinds de woordgrens-familie
|
||||
# \bstations?\b en zijn verwijderd — dat gedrag is in validate_filter.py
|
||||
# vastgelegd.
|
||||
MATCH_FILTER_BLACKLIST: frozenset[str] = frozenset({
|
||||
# Straatnamen rond spoorlijnen die de \b\w*trein\w*\b-familie zou matchen:
|
||||
# een straat die toevallig 'Trein' heet maakt een bericht geen OV-bericht.
|
||||
# Straatnamen rond stations (genitief-s-vorm van "\bstation\w*\b"): een straat
|
||||
# die toevallig bij een station ligt maakt een bericht nog geen OV-bericht.
|
||||
"stationsstraat", "stationsweg", "stationslaan", "stationsplein",
|
||||
"stationssingel", "stationskade", "stationsdreef", "stationshof",
|
||||
"stationspad", "stationsboulevard", "stationsgracht", "stationsdijk",
|
||||
"stationssteeg", "stationswal", "stationsbuurt", "stationswijk", "stationspark",
|
||||
# Idem voor "\btrein\w*\b".
|
||||
"treinstraat", "treinweg",
|
||||
# Idem voor "\bstation\w*\b": werkwoords-/bijvoeglijke vormen zonder OV-waarde
|
||||
# ("motor stationair laten draaien", "auto's hier stationeren") en
|
||||
# samenstellingen met een station in de naam ("stationcar", "stationwagen").
|
||||
"stationair", "stationeel", "stationeren", "stationeert", "stationeerde",
|
||||
"stationeerden", "stationerend", "stationcar", "stationwagen",
|
||||
"stationnement", "stationering",
|
||||
})
|
||||
|
||||
# MATCH_FILTER_WHITELIST: termen die een bericht per definitie OV maken. Sinds
|
||||
# besluit 2026-09-12 is dit geen auto-pass meer: een whitelist-hit in de TITEL
|
||||
# telt +2 (één onmiskenbare term 2, nog een signaal nodig voor de drempel 3), en
|
||||
# in de SAMENVATTING helemaal niet (die scoort via de overeenkomstige familie —
|
||||
# een whitelist-term zit altijd óók in een familie). Zie _filter_passes.
|
||||
# "Onmiskenbaar" = in élke context OV: merken (NS/ProRail/Arriva), onmiskenbaar
|
||||
# jargon (spoorstoring, perron, bushalte, conducteur, ovpay), multi-woord-frasen
|
||||
# (nederlandse spoorwegen, openbaar vervoer, ice-berlijn ...).
|
||||
# Regel: twijfel je of de term ook buiten het OV kan voorkomen -> NIET hier, maar
|
||||
# zwak (eigen familie). Daarom 'hermes' hieruit (achternaam, besluit 2026-09-12).
|
||||
# 'treinstel'/'veerdienst'/'buurtbus'/'lijn 12' staan hier alleen als TITEL-anker
|
||||
# (= het oude effect van één onmiskenbare term in de titel, nu via +2) — in de
|
||||
# samenvatting wegen ze als familie (2/1). Vergelijking is case-insensitive.
|
||||
# MATCH_FILTER_WHITELIST: termen die een bericht per definitie OV maken. Eén enkele
|
||||
# hit in de TITEL laat ALTIJD door (geen familiescore nodig) — dit is het expliciete
|
||||
# "onmiskenbaar OV"-signaal, zodat we voor deze termen niet op de gewogen
|
||||
# heuristiek hoeven te leunen. Vergelijking is case-insensitive, dus ook 'nederlandse
|
||||
# spoorwegen', 'prorail' en 'NS' in all-caps titels tellen mee.
|
||||
# Een whitelist-hit ALLEEN in de samenvatting is géén onvoorwaardelijk signaal
|
||||
# (besluit 2026-09-12: 'openbaar vervoer' als bijzin in een economiebericht is geen
|
||||
# OV-nieuws); die term scoort dan via de gewogen families — voor whitelist-termen
|
||||
# die geen eigen familie hebben (bv. 'treinstel': alleen zwak via \w*trein\w*) is
|
||||
# een samenvatting-alleen hit dus zwak.
|
||||
# Termen MOETEN in elke context OV betekenen. Niet hierop (algemeen/dubbelzinnig):
|
||||
# los 'trein', 'bus', 'metro', 'tram', 'station', 'lijn', 'ov', 'reiziger',
|
||||
# 'vervoerder', 'sprinter',
|
||||
# 'inchecken' (Schiphol/hotel), 'sein' (brandweerjargon), 'overweg', 'treinramp'
|
||||
# (films) — die blijven in de gewogen families.
|
||||
MATCH_FILTER_WHITELIST: frozenset[str] = frozenset({
|
||||
# Vervoerders en OV-merken (incl. multi-woord termen)
|
||||
"nederlandse spoorwegen", "ns", "prorail", "randstadrail", "arriva", "qbuzz",
|
||||
"connexxion", "transdev", "keolis", "ret", "gvb", "htm", "ebs", "syntus",
|
||||
"u-ov", "allgo", "blauwnet", "r-net", "valleilijn", "rrreis",
|
||||
"u-ov", "allgo", "blauwnet", "r-net", "valleilijn", "hermes", "rrreis",
|
||||
"qliner", "flixbus", "eurostar", "thalys", "gvu", "ovpay",
|
||||
"deutsche bahn", "european sleeper", "ns international", "italo", "nightjet",
|
||||
"teso", "ice-berlijn",
|
||||
# Spoorinfra-jargon (samenvoegingen, onmiskenbaar OV)
|
||||
"spoorstoring", "spoorverkeer", "spoorwegovergang", "spoorlijn", "spoorbaan",
|
||||
"spoorstaking", "spoorsector", "spoorbeheerder", "spoornet", "spoorvervoerder",
|
||||
"ertms", "perron", "wisselstoring", "seinstoring", "treinstoring", "treinverkeer",
|
||||
"treinstaking", "treinongeval", "treinbotsing", "treinstel", "treinvertraging",
|
||||
"treinhinder", "treinuitval", "treinverstoring", "maaslijn", "nedersaksenlijn",
|
||||
"kamperlijn",
|
||||
"spoorstaking", "perron", "wisselstoring", "seinstoring", "treinstoring", "treinverkeer",
|
||||
"treinstaking", "treinongeval", "treinbotsing", "treinstel",
|
||||
# Bus/tram/metro-jargon
|
||||
"bushalte", "bushaltes", "busbaan", "buslijn", "busnet",
|
||||
"busstaking", "buurtbus", "veerdienst", "lijn 12",
|
||||
"tramhalte", "tramlijn", "tramnet", "tramverkeer",
|
||||
"busstaking", "tramhalte", "tramlijn", "tramnet", "tramverkeer",
|
||||
"metrolijn", "metroverkeer",
|
||||
# Tarief/vervoerder-jargon
|
||||
"ov-chipkaart", "ov-staking", "lightrail",
|
||||
|
|
@ -325,9 +263,6 @@ _WHITELIST_RE = re.compile(
|
|||
)
|
||||
_WHITELIST_PHRASES: tuple[str, ...] = tuple(w for w in MATCH_FILTER_WHITELIST if " " in w)
|
||||
|
||||
# Dienststoring-context als sterk TITEL-signaal (zie _DISRUPTION_RE_SRC).
|
||||
_DISRUPTION_RE = re.compile(_DISRUPTION_RE_SRC, re.IGNORECASE)
|
||||
|
||||
|
||||
class Config(BaseProxyConfig):
|
||||
def do_update(self, helper: ConfigUpdateHelper) -> None:
|
||||
|
|
@ -467,111 +402,73 @@ class RSSBot(Plugin):
|
|||
low = text.lower()
|
||||
return any(ph in low for ph in _WHITELIST_PHRASES)
|
||||
|
||||
@staticmethod
|
||||
def _disruption_hit(text: str) -> bool:
|
||||
"""True als een dienststoring-zin ('sprinters rijden niet', 'metro rijdt
|
||||
niet tussen Rotterdam en Schiedam') in text staat. In de TITEL telt zo'n
|
||||
zin als sterk OV-signaal (+2, net als een whitelist-term); als familie
|
||||
(2) scoort hij overal — zie _DISRUPTION_RE_SRC."""
|
||||
return _DISRUPTION_RE.search(text) is not None
|
||||
|
||||
@staticmethod
|
||||
def _fam_hits(text: str) -> list[tuple[int, int]]:
|
||||
"""(familie-index, gewicht) per OV-signaal in text (1× per familie).
|
||||
"""(familie-index, gewicht) voor elke OV-familie die in text voorkomt (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.
|
||||
|
||||
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 — 'ov-chipkaart' bevat 'chipkaart' (2+1=3),
|
||||
'openbaar vervoer' bevat 'vervoer' (2+1=3), 'treinstoring' matcht ook
|
||||
de zwakke \\w*trein\\w*-familie (2+1=3). Max-2-stelsel: één term kan
|
||||
nooit solo doorlaten."""
|
||||
spans: list[tuple[int, int, int, int]] = [] # (start, end, familie-idx, gewicht)
|
||||
dezelfde tekst scoort gewoon verder."""
|
||||
hits: list[tuple[int, int]] = []
|
||||
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
|
||||
spans.append((m.start(), m.end(), idx, weight))
|
||||
hits.append((idx, weight))
|
||||
break
|
||||
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]
|
||||
return hits
|
||||
|
||||
@staticmethod
|
||||
def _filter_passes(title: str, summary: str) -> bool:
|
||||
"""Driestaps-beslissing, precision-first (refactor 2026-09-13; beslissing
|
||||
equivalent met v1.13.0, maar de drie historische uitzonderings-takken
|
||||
reduceren tot één regel — zie stap 3):
|
||||
"""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).
|
||||
|
||||
1. STERK SIGNAAL: een whitelist-term óf een dienststoring-zin
|
||||
('sprinters rijden niet') in de titel is voldoende. Elke
|
||||
whitelist-term staat óók in een familie, dus zo'n titel telt +2 én
|
||||
het familie-gewicht — samen altijd >= drempel ('NS meldt storing',
|
||||
'Werkzaamheden aan perron', 'Buurtbus rijdt niet meer'). In de
|
||||
samenvatting ALLEEN is zo'n term géén bewijs ('openbaar vervoer'
|
||||
als bijzin in een economiebericht, besluit 2026-09-12) en scoort
|
||||
die alleen via de overeenkomstige familie.
|
||||
|
||||
2. CONTEXT: de som van unieke OV-signalen over titel + samenvatting
|
||||
>= drempel (3). Eén term telt één signaal per artikel ('busstation'
|
||||
60× of in beide velden = één keer); overlappende familie-treffers
|
||||
van dezelfde term wegen 1× met het hoogste gewicht (span-merge in
|
||||
_fam_hits, besluit 2026-09-13 — 'ov-chipkaart' bevat 'chipkaart'
|
||||
maar weegt als één term 2, geen 3). Geen
|
||||
familie weegt meer dan 2 (max-2-stelsel, besluit 2026-09-12): een
|
||||
enkel sterk woord kan nooit solo doorlaten ('hermes' als
|
||||
achternaam passeerde vroeger op één vervoerder-hit van 3 —
|
||||
Margraten; écht OV-nieuws combineert in de praktijk altijd met een
|
||||
tweede signaal, gevalideerd op 205 productie-artikelen).
|
||||
|
||||
3. TITEL-ANKER: staat er geen enkel OV-signaal in de titel (geen
|
||||
familie-hit, geen whitelist-term), dan is de score uitsluitend
|
||||
samenvatting-bewijs en mag die geen zwak/ambigue woord bevatten —
|
||||
'station'+'pendelbus'+'bus' in een golfverslag, 'openbaar
|
||||
vervoer'+'zakelijke reizigers' in een economiebericht en 'sterk +
|
||||
zwak uitsluitend in de samenvatting' (beide besluiten 2026-09-12)
|
||||
vallen hieronder. Twee sterke termen in de samenvatting blijven
|
||||
bewust door (delftopzondag 'tram 1, halte Buitenhofdreef',
|
||||
besluit 2026-09-13).
|
||||
|
||||
Weert zo '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 'sein brandmeester', figuratief 'weer op de rails',
|
||||
en een cultuurstukje over een 'stationspiano'. Bewust doorgelaten:
|
||||
krant 'De Metro'/café 'De Tram' (2, combineert) en atleet-'sprinter'
|
||||
in combinatie met ander OV-woord."""
|
||||
Sterke families (gewicht 3) halen de drempel al met één woord in titel óf
|
||||
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. 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
|
||||
'sein brandmeester', figuratief 'weer op de rails' en een golfverslag
|
||||
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: 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)
|
||||
# Unieke OV-signalen over beide velden: één term telt één signaal per
|
||||
# artikel ('busstation' in titel én samenvatting, of 60×, blijft één
|
||||
# treffer); _fam_hits levert per term al het hoogste gewicht bij
|
||||
# overlappende familie-treffers.
|
||||
# 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())
|
||||
strong_title = RSSBot._whitelist_hit(title) or RSSBot._disruption_hit(title)
|
||||
if strong_title:
|
||||
# Stap 1: sterk titel-signaal — +2 bovenop het familie-gewicht van
|
||||
# dezelfde term, waardoor een eenduidige OV-titel alleen voldoet.
|
||||
score += 2
|
||||
if score < MATCH_FILTER_THRESHOLD:
|
||||
return False
|
||||
if not t_hits and not strong_title and 1 in combined.values():
|
||||
# Stap 3: geen enkel titel-signaal én een zwak woord in de score —
|
||||
# samenvatting-bewijs zonder titel-anker is geen OV-bewijs.
|
||||
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(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
|
||||
|
||||
|
|
@ -579,24 +476,26 @@ class RSSBot(Plugin):
|
|||
def _filter_summary() -> str:
|
||||
"""Mensvriendelijke weergave van het vaste gewogen match-filter."""
|
||||
return (
|
||||
"**OV-matchfilter (vast in code; max-2-stelsel, precision-first):**\n"
|
||||
"- **Sterk signaal in de TITEL → door**: whitelist-term (NS, ProRail, Nederlandse Spoorwegen, spoorstoring, perron, "
|
||||
"bushalte, conducteur, ov-chipkaart, ovpay, openbaar vervoer, Deutsche Bahn, ICE Berlijn, ...) of een dienststoring-zin "
|
||||
"('sprinters rijden niet', 'metro rijdt niet') — die telt +2 bovenop het familie-gewicht. In de samenvatting alleen "
|
||||
"weegt zo'n term mee als gewone familie.\n"
|
||||
"- **Context (som ≥ 3)**: unieke OV-families over titel + samenvatting. Sterk (2): vervoerders (NS, Arriva, ProRail, Qbuzz, ...), "
|
||||
"spoorinfra (spoorlijn, spoorstoring, perron, ertms, spoorsector), bus-samenstellingen (bushalte, buslijn, buschauffeur, busbaan), "
|
||||
"treinstoring/treinverkeer/treinvertraging, intercity/streekvervoer/veerdienst/buurtbus/belbus, 'lijn 12', ICE-frasen, rangeer..., "
|
||||
"tram, metro, openbaar vervoer, lightrail, halte, pont, spoorbrug, ov-chipkaart/ovpay, ov-staking, conducteur, dienststoring-zin. "
|
||||
"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'/'passagier', 'vervoerder', 'sprinter', 'spoor', 'staking', "
|
||||
"'vervoer', 'busongeluk', 'busvervoer', ... — elke dubbelzinnige term heeft een eigen familie en moet combineren.\n"
|
||||
"- **Eén term = één signaal**: herhaling, beide velden en overlappende familie-treffers wegen niet dubbel. "
|
||||
"Zonder enig titel-signaal (familie-hit, whitelist-term of storingszin in de titel) mag de score geen zwak woord "
|
||||
"bevatten — samenvatting-bewijs is geen OV-bewijs (twee sterke termen in de samenvatting blijven bewust door).\n"
|
||||
"- **Uitgesloten (blacklist)**: 'treinstraat'/'treinweg' (straatnamen rond spoorlijnen); historische stations*-gevallen "
|
||||
"('stationsstraat', 'stationair', 'stationcar', ...) worden al door de woordgrens-familie geweerd"
|
||||
"**OV-matchfilter (vast in code, gewogen families):**\n"
|
||||
"- Sterk (3): vervoerders (NS, Arriva, ProRail, Qbuzz, ...), spoorinfra (spoorlijn, spoorwegovergang, perron, spoorstoring), "
|
||||
"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, spoorbrug, 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 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: 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"
|
||||
)
|
||||
|
||||
async def _send(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
id: nl.eddydevink.rss
|
||||
version: 1.14.0
|
||||
version: 1.11.0
|
||||
modules:
|
||||
- buzzrss
|
||||
main_class: RSSBot
|
||||
|
|
|
|||
|
|
@ -25,9 +25,8 @@ 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"}
|
||||
WANT_METHODS = {"_fam_hits", "_filter_passes", "_filter_text", "_whitelist_hit"}
|
||||
|
||||
nodes = []
|
||||
for n in tree.body:
|
||||
|
|
@ -74,16 +73,12 @@ def check(cond: bool, msg: str):
|
|||
# --- Invariant 1: dubbelzinnige woorden mogen nooit sterk/medium of whitelist zijn
|
||||
AMBIGUOUS = [
|
||||
"trein", "bus", "station", "stations", "sprinter", "sprinters",
|
||||
"reiziger", "reizigers", "passagier", "passagiers", "vervoerder", "vervoerders",
|
||||
"reiziger", "reizigers", "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:
|
||||
|
|
@ -98,12 +93,6 @@ UNAMBIGUOUS = [
|
|||
"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:
|
||||
|
|
@ -111,22 +100,12 @@ for w in UNAMBIGUOUS:
|
|||
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} ==")
|
||||
# --- Invariant 3: patronen compileren en gewichten geldig
|
||||
print("== invariant 3: alle 24+ families compileren, gewicht in {1,2,3} ==")
|
||||
for p, w in FAMILIES:
|
||||
check(w in (1, 2), f"familie {p[:40]!r} heeft geldig gewicht {w} (max-2-stelsel)")
|
||||
check(w in (1, 2, 3), f"familie {p[:40]!r} heeft geldig gewicht {w}")
|
||||
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)
|
||||
|
|
@ -204,78 +183,6 @@ GOLDEN = [
|
|||
("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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue