diff --git a/CHANGELOG.md b/CHANGELOG.md index 26fee56..0876661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # CHANGELOG — Set-OPS +## 2026-09-28 (9) — P02 : l'écriture d'une table s'arrêtait au premier commentaire + +**`make verifier` : CONFORME, 83 OK, 0 échec** — P02 échouait depuis le 2026-09-16. + +**La cause.** `_fusion_table` (écriture ligne à ligne des registres du plan, par le GUI) +délimitait une entrée par les lignes plus indentées qui la suivent, et **s'arrêtait à la +première ligne de commentaire**. Le 2026-09-16, `chezlepro.ca` a reçu des commentaires À +L'INTÉRIEUR de son entrée (signature DNSSEC, relevé de la zone) : l'entrée était coupée en +deux, et les lignes suivantes testées comme entrées de la table. ` - {nom: "@", type: +A, …}` y passait, clef `- {nom`, YAML = une liste → `'list' object has no attribute 'get'`. +Le GUI ne pouvait plus écrire `domaines.yml`. + +**Le correctif.** L'étendue d'une entrée traverse commentaires et lignes vides tant que la +prochaine ligne significative est encore plus indentée ; un commentaire suivi de l'entrée +SUIVANTE reste avec elle. Et seules les lignes à l'indentation des enfants directs peuvent +être des entrées. Réécrire le vrai `domaines.yml` sans changement le rend octet pour octet. + +**Le test aussi avait une prémisse périmée.** Retirer une entité devait garder TOUS les +commentaires du fichier — juste tant qu'aucune entrée n'en portait à l'intérieur. Un +commentaire intérieur part désormais avec son entrée (laissé, il expliquerait la voisine) ; +ceux qui l'entourent restent, et c'est ce que le test exige maintenant. + ## 2026-09-28 (8) — L'amont du cache des locataires suit le site, au lieu d'une adresse morte `serveur_artefacts_amont` valait `http://10.0.33.21:3142` chez Chezlepro ET Technolibre : diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 06b5490..e10f6a5 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -1166,21 +1166,36 @@ def _fusion_table(bloc: list[str], entrees: dict) -> list[str]: tete, corps = bloc[0], bloc[1:] sortie, vues = [tete], set() flow, colonne = 0, 0 + # SEULS LES ENFANTS DIRECTS SONT DES ENTREES (2026-09-28). Une ligne plus indentee + # appartient a une entree ; la tester contre `_ENTREE_TABLE` la prenait pour une entree + # a part : ` - {nom: "@", type: A, ...}` donnait la clef `- {nom`, dont le YAML est + # une LISTE — et `.get` echouait (P02, depuis la zone signee du 2026-09-16). + enfant = next((m.group(1) for m in map(_ENTREE_TABLE.match, corps) if m), None) i = 0 while i < len(corps): ligne = corps[i] m = _ENTREE_TABLE.match(ligne) - if not m: + if not m or m.group(1) != enfant: sortie.append(ligne) i += 1 continue indent, cle, ecart, reste = m.group(1), m.group(2).strip(), m.group(3), m.group(4) - # L'entree s'etend sur les lignes PLUS indentees qui suivent immediatement. + # L'entree s'etend sur les lignes PLUS indentees qui suivent — Y COMPRIS a travers + # une ligne vide ou un commentaire, tant que la prochaine ligne SIGNIFICATIVE est + # encore plus indentee. S'arreter au premier commentaire coupait `chezlepro.ca:` en + # deux, a l'endroit ou il explique sa signature DNSSEC. Un commentaire suivi de + # l'entree SUIVANTE, lui, reste avec elle : il la coiffe. fin = i j = i + 1 while j < len(corps): suite = corps[j] if not suite.strip() or suite.strip().startswith("#"): + k = j + 1 + while k < len(corps) and (not corps[k].strip() or corps[k].strip().startswith("#")): + k += 1 + if k < len(corps) and len(corps[k]) - len(corps[k].lstrip()) > len(indent): + j = k + continue break creux = len(suite) - len(suite.lstrip()) if creux <= len(indent): diff --git a/scripts/tests/test_ecriture_plan.py b/scripts/tests/test_ecriture_plan.py index e6e02ad..3f4eff7 100644 --- a/scripts/tests/test_ecriture_plan.py +++ b/scripts/tests/test_ecriture_plan.py @@ -23,6 +23,7 @@ gabarit invente : c'est la seule facon de constater ce que l'operateur perdrait. from __future__ import annotations +import re import shutil import sys import tempfile @@ -49,6 +50,39 @@ def commentaires(texte: str) -> list[str]: return [l.strip() for l in texte.splitlines() if l.strip().startswith("#")] +def commentaires_hors_entree(texte: str, cle: str) -> list[str]: + """Les commentaires du fichier, MOINS ceux qui vivent DANS l'entree `cle`. + + Un commentaire interieur a une entree (2026-09-16 : `chezlepro.ca` explique sa + signature DNSSEC entre `secondaires:` et `dnssec:`) part avec elle : laisse en place, + il expliquerait l'entree voisine. Ceux qui l'ENTOURENT restent. Meme regle d'etendue + que `_fusion_table` : un commentaire suivi d'une ligne encore plus indentee est dedans. + """ + lignes = texte.splitlines() + debut = next((i for i, l in enumerate(lignes) + if re.match(rf"^(\s+){re.escape(cle)}:", l)), None) + if debut is None: + return commentaires(texte) + indent = len(lignes[debut]) - len(lignes[debut].lstrip()) + dedans, j = set(), debut + 1 + while j < len(lignes): + l = lignes[j] + if not l.strip() or l.strip().startswith("#"): + k = j + while k < len(lignes) and (not lignes[k].strip() or lignes[k].strip().startswith("#")): + k += 1 + if k < len(lignes) and len(lignes[k]) - len(lignes[k].lstrip()) > indent: + dedans.update(range(j, k)) + j = k + continue + break + if len(l) - len(l.lstrip()) <= indent: + break + j += 1 + return [l.strip() for i, l in enumerate(lignes) + if l.strip().startswith("#") and i not in dedans] + + class TestEcriturePlan(unittest.TestCase): def setUp(self): @@ -115,7 +149,8 @@ class TestEcriturePlan(unittest.TestCase): # Les autres entites restent, et les commentaires aussi : retirer une # entite n'est pas une raison d'effacer ce qui l'entourait. self.assertEqual(sorted(relu[racine]), sorted(registre[racine])) - self.assertEqual(commentaires(avant), commentaires(apres)) + # Ce qui ENTOURAIT l'entite reste ; ce qui etait DEDANS part avec elle. + self.assertEqual(commentaires_hors_entree(avant, victime), commentaires(apres)) def test_controle_negatif_safe_dump_detruit(self): """LE CONTROLE. L'ancienne ecriture DOIT perdre des commentaires.