P02 : une table du plan ne se coupe plus a son premier commentaire

_fusion_table traversait mal les commentaires interieurs a une entree :
chezlepro.ca (DNSSEC) etait coupee et ses enregistrements pris pour des entrees.
make verifier : CONFORME, 83 OK, 0 echec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-09-28 13:17:52 -04:00
parent ffc171a2cb
commit 0156596f01
3 changed files with 75 additions and 3 deletions

View file

@ -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 :

View file

@ -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):

View file

@ -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.