From de290735074fa728dd17da85073ff8960c9561eb Mon Sep 17 00:00:00 2001 From: jsboige Date: Sun, 13 Sep 2026 05:58:02 +0200 Subject: [PATCH] Fix: add 'infrastructure' to _GENRE_ALIASES and close the variant class (#15897) canonicalize_genre('infrastructure') returned the word verbatim: the two abbreviated forms ('infra', 'infra-docker') were in the table, the long form a human writes spontaneously was not. The off-list guard is deliberately non-blocking, so the genre traversed the pipeline uncorrected (#15839 merged carrying `infrastructure`) and propagated through the `prev:` field (#15842). Two tests close the class rather than the instance: - test_genre_alias_variant_families_share_one_target pins each declared family of spellings to a single canonical target, and requires every non-canonical member to be an EXPLICIT key of the table (so no member resolves by accident). - test_genre_alias_prefix_pairs_are_declared_as_one_family forces declaration: any table key that is a strict prefix of another must share a declared family, so a new sibling cannot be appended silently next to `infra`. Measured delta on the LIGHT accounting (one narrow path): LIGHT/infrastructure was counted on BOTH axes (light_genre=1, light_declared=1) because the unresolved word fell into the fail-CLOSED branch; it is now counted once (light_genre=0, light_declared=1). The declared-tier axis still holds it, so nothing is laundered. MED/DEEP are unchanged (tier-aware since #13585). Co-Authored-By: Claude Sonnet 5 --- scripts/tests/test_variation_light_cap.py | 84 +++++++++++++++++++++++ scripts/variation_light_cap.py | 9 +++ 2 files changed, 93 insertions(+) diff --git a/scripts/tests/test_variation_light_cap.py b/scripts/tests/test_variation_light_cap.py index c4271ed009..377b95cb42 100644 --- a/scripts/tests/test_variation_light_cap.py +++ b/scripts/tests/test_variation_light_cap.py @@ -552,6 +552,10 @@ def test_canonicalize_genre_aliases(): assert vlc.canonicalize_genre("test-coverage") == "test" assert vlc.canonicalize_genre("data") == "ledger" assert vlc.canonicalize_genre("slidev") == "slides" + # #15897 -- les deux abregees etaient couvertes, pas la forme longue. + assert vlc.canonicalize_genre("infra") == "tooling" + assert vlc.canonicalize_genre("infra-docker") == "tooling" + assert vlc.canonicalize_genre("infrastructure") == "tooling" # No alias -> identity. assert vlc.canonicalize_genre("readme") == "readme" assert vlc.canonicalize_genre("DOCS") == "DOCS".lower() # case-insensitive @@ -565,6 +569,86 @@ def test_canonicalize_genre_aliases(): assert vlc.canonicalize_genre("lean-tooling") == "tooling" +# --- #15897 : fermer la CLASSE des variantes, pas l'instance ----------------- +# +# Lacune fondee : `infra` et `infra-docker` etaient dans la table, +# `infrastructure` -- la forme longue, celle qu'un humain ecrit spontanement -- +# ne l'etait pas. Le garde hors-liste etant deliberement NON bloquant, le genre +# a traverse le pipeline (#15839 a merge en portant `infrastructure`) puis s'est +# propage par le champ `prev:` (#15842 : `prev: DEEP/infrastructure #15839`). +# +# Deux tests ferment la classe : +# 1. `_VARIANT_FAMILIES` declare chaque famille d'ecritures d'une MEME tete ; +# tout membre doit canonicaliser vers la MEME cible, et tout membre non +# canonique doit etre une cle EXPLICITE de la table (donc pas resolu par +# accident). +# 2. le test de prefixes force la DECLARATION : des qu'une cle de la table est +# un prefixe strict d'une autre, les deux doivent partager une famille +# declaree. Ajouter `infra-cloud` a cote de `infra` sans rejoindre la +# famille echoue -- c'est l'invariant qui manquait. +# +# Portee honnete : une famille ENTIEREMENT nouvelle (`foo` / `foobar`, ou `foo` +# n'existe pas encore) ne se decouvre pas mecaniquement sans dictionnaire -- elle +# se declare. Ces tests garantissent qu'une fois une famille ouverte, aucun +# membre ne peut manquer ni diverger en silence. +_VARIANT_FAMILIES = ( + # `infra` abrege, `infrastructure` en toutes lettres, et le compose + # `infra-docker` qui porte la meme tete (le tail `docker` est opaque, + # cf #13585 : la forme reste table-driven). + ("infra", "infra-docker", "infrastructure"), + # `docs` est canonique (donc absent de la table d'alias) ; `documentation` + # est l'alias que le texte de la regle nomme lui-meme (#13475). + ("docs", "documentation"), +) + + +def _variant_family_of(member): + for family in _VARIANT_FAMILIES: + if member in family: + return family + return None + + +def test_genre_alias_variant_families_share_one_target(): + # Chaque famille = les ecritures d'une meme tete. Toutes doivent + # canonicaliser identiquement, sinon `MED/infrastructure` et `MED/infra` ne + # compteraient pas de la meme facon dans G-VAR-2/3 -- exactement la + # consequence mesuree sur #15839/#15842. + for family in _VARIANT_FAMILIES: + targets = {vlc.canonicalize_genre(m) for m in family} + assert len(targets) == 1, (family, targets) + assert None not in targets, family + for member in family: + if member in vlc.GENRES: + continue + assert member in vlc._GENRE_ALIASES, ( + f"'{member}' n'est ni canonique ni dans _GENRE_ALIASES : " + f"la famille {family} resout par accident" + ) + + +def test_genre_alias_prefix_pairs_are_declared_as_one_family(): + # Invariant mecanique : si une cle est un prefixe strict d'une autre, les + # deux sont des ecritures liees -> meme famille declaree. `infra` est le + # seul prefixe de la table (`infra-docker`, `infrastructure`) ; le jour ou + # une variante s'ajoute a cote sans sa forme longue, ou dans une autre + # famille, ce test echoue et force la decision explicite (#15897). + keys = set(vlc._GENRE_ALIASES) + for short in sorted(keys): + for long in sorted(keys): + if short == long or not long.startswith(short): + continue + family = _variant_family_of(short) + assert family is not None, ( + f"'{short}' est un prefixe de '{long}' : declare la famille " + f"de variantes (cf #15897)" + ) + assert _variant_family_of(long) is family, ( + f"'{short}' et '{long}' partagent un prefixe : meme famille " + f"requise (cf #15897)" + ) + + def test_light_genres_set_is_locked(): # The G-VAR-3 lockout genres. Adding a genre here is a deliberate, # auditable change to the protocol, not a tunable. diff --git a/scripts/variation_light_cap.py b/scripts/variation_light_cap.py index b4a7e3d140..8663373199 100644 --- a/scripts/variation_light_cap.py +++ b/scripts/variation_light_cap.py @@ -564,6 +564,15 @@ def replay(merged_prs: list[dict]) -> list[dict]: # equal the previous genre, so G-VAR-3 was unreachable by word choice). "scripts": "tooling", "infra": "tooling", + # #15897 -- la forme LONGUE de `infra`, celle qu'un humain ecrit + # spontanement. Les deux abregees (`infra`, `infra-docker`) etaient + # couvertes, pas celle-ci : un genre hors-table traversant le pipeline + # sans etre corrige (#15839 a merge en portant `infrastructure`, puis + # #15842 a propage la lacune par `prev: DEEP/infrastructure #15839`). + # La classe entiere est desormais tenue par + # `test_genre_alias_variant_families` (toute variante declaree doit + # canonicaliser vers la meme cible que ses soeurs). + "infrastructure": "tooling", "csp": "research-code", # #13475 -- the two words the RULE TEXT itself names (variation-protocol # ยง1 lists `documentation -> docs` among the normalising synonyms) and