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:
parent
ffc171a2cb
commit
0156596f01
3 changed files with 75 additions and 3 deletions
22
CHANGELOG.md
22
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 :
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue