[ADD] commit-msg : contrôler le corps, sa longueur et l'identifiant
Le garde-fou ne lisait que le sujet : le corps portait des noms de bases
réelles, des adresses et des chemins de compte, et sa longueur médiane
faisait une fois et demie le plafond de la convention.
Il lit désormais le corps, trailers et diff de --cleanup=scissors exclus :
longueur par langue, adresse IP, courriel, chemin de compte, et les termes
d'une liste qui vit hors du dépôt puisque c'est elle qu'elle protège —
absente, le contrôle se tait. Le prédicat qui sépare une adresse d'une
version de manifeste est partagé ; 40 tests couvrent les deux lectures.
--- EN ---
The guard rail read the subject only, while the body carried real database
names, addresses and account paths, and its median length was one and a
half times the convention's ceiling.
It now reads the body, trailers and the --cleanup=scissors diff excluded:
length per language, IP address, e-mail, account path, and the terms of a
list living outside the repository, since the list is what it protects —
absent, that check stays silent. The predicate telling an address from a
manifest version is shared; 40 tests cover both readings.
Assisted-by: Claude Opus 5
2026-08-30 02:02:38 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Ce texte porte-t-il une donnée qui désigne quelqu'un ou quelque chose ?
|
|
|
|
|
|
|
|
|
|
La règle est dans `.claude/rules/04-code-conventions.md` : ce qui est versionné
|
|
|
|
|
ne nomme ni client, ni base réelle, ni machine, ni adresse. Deux garde-fous s'en
|
[ADD] commentaires : un relevé non bloquant, sa règle, le code nettoyé
Rien ne relevait les commentaires hors convention. L'outil lit commentaires et
docstrings, jamais le code autour, et rend deux familles inégales :
l'identifiant — adresse, courriel, chemin de compte, nom de la liste privée —
est une trouvaille ; le récit — témoignage, date, personne — un signal à relire.
Le hook pre-commit le lance sur l'index, sort toujours en 0 — un contrôle
bloquant à cette échelle se fait désinstaller — et parle quand l'outil échoue.
La règle du générateur et sa doc portent le nettoyage au fur et à mesure, et
l'exemple d'un interdit s'invente : base, adresse, compte et hôte en prennent un.
L'écran du contexte est posé, sans entrée de menu. Vérifié : 252 tests des six
fichiers d'essai touchés passent.
--- EN ---
Nothing reported the comments that break the convention. The tool reads
comments and docstrings, never the code around them, and returns two unequal
families: identifying data — address, e-mail, account path, private-list name
— is a finding; narrative — witness marker, date, person — a signal to re-read.
The pre-commit hook runs it on the index, always exits 0 — a blocking check at
that scale gets uninstalled — and speaks when the tool fails. The generator
rule and its doc carry the clean-up as you go, and a forbidden thing's example
is invented: a real database, address, account and host each take one.
The context screen is in place, with no menu entry. Checked: 252 tests of the
six touched fixture files pass.
Assisted-by: Claude Opus 5
2026-08-30 02:02:51 -04:00
|
|
|
servent — `script/git/commit_msg_lib.py` sur le message, et
|
[ADD] commit-msg : contrôler le corps, sa longueur et l'identifiant
Le garde-fou ne lisait que le sujet : le corps portait des noms de bases
réelles, des adresses et des chemins de compte, et sa longueur médiane
faisait une fois et demie le plafond de la convention.
Il lit désormais le corps, trailers et diff de --cleanup=scissors exclus :
longueur par langue, adresse IP, courriel, chemin de compte, et les termes
d'une liste qui vit hors du dépôt puisque c'est elle qu'elle protège —
absente, le contrôle se tait. Le prédicat qui sépare une adresse d'une
version de manifeste est partagé ; 40 tests couvrent les deux lectures.
--- EN ---
The guard rail read the subject only, while the body carried real database
names, addresses and account paths, and its median length was one and a
half times the convention's ceiling.
It now reads the body, trailers and the --cleanup=scissors diff excluded:
length per language, IP address, e-mail, account path, and the terms of a
list living outside the repository, since the list is what it protects —
absent, that check stays silent. The predicate telling an address from a
manifest version is shared; 40 tests cover both readings.
Assisted-by: Claude Opus 5
2026-08-30 02:02:38 -04:00
|
|
|
`script/analyse/check_comment_hygiene.py` sur les commentaires — et ils
|
|
|
|
|
partagent CE module. Deux prédicats séparés pour la même question dérivent.
|
|
|
|
|
|
|
|
|
|
Chaque trouvaille porte sa position dans le texte : l'appelant en déduit la
|
|
|
|
|
ligne exacte, plutôt que de rechercher l'extrait à l'aveugle.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import io
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
RACINE = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
|
|
|
|
|
|
# La liste des clients et des machines ne peut pas vivre dans le dépôt public :
|
|
|
|
|
# c'est ce qu'elle protège. Absente, le contrôle qui s'en sert est muet.
|
|
|
|
|
NOMS_INTERDITS = os.path.join(RACINE, "private", "noms_interdits.txt")
|
|
|
|
|
|
|
|
|
|
IPV4 = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
|
|
|
|
|
|
|
|
|
|
# Les blocs de documentation de la RFC 5737, qui existent pour l'exemple.
|
|
|
|
|
IPV4_DOCUMENTAIRES = ("192.0.2.", "198.51.100.", "203.0.113.")
|
|
|
|
|
|
|
|
|
|
EMAIL = re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.-]*[\w-]\b")
|
|
|
|
|
|
|
|
|
|
# L'adresse du propriétaire du dépôt n'est pas une donnée de client.
|
|
|
|
|
EMAIL_PERMIS = re.compile(r"@(?:technolibre|erplibre)\.", re.IGNORECASE)
|
|
|
|
|
|
|
|
|
|
# Un segment entre chevrons ou une variable est un gabarit, pas un compte.
|
|
|
|
|
HOME = re.compile(r"/(?:home|Users)/(?![<$\"'{])([\w.-]+)/")
|
|
|
|
|
HOME_PERMIS = frozenset({"runner", "user", "utilisateur", "USER"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def adresse_de_machine(valeur):
|
|
|
|
|
"""Cette suite de quatre nombres désigne-t-elle une machine du parc ?
|
|
|
|
|
|
|
|
|
|
Non pour un octet hors bornes, une version de module Odoo (`18.0.1.3`), la
|
|
|
|
|
boucle locale, un masque, un bloc documentaire, et une adresse de RÉSEAU —
|
|
|
|
|
un dernier octet nul nomme une plage, pas un hôte.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
nombres = [int(o) for o in valeur.split(".")]
|
|
|
|
|
except ValueError:
|
|
|
|
|
return False
|
|
|
|
|
if len(nombres) != 4 or any(n > 255 for n in nombres):
|
|
|
|
|
return False
|
|
|
|
|
if 12 <= nombres[0] <= 18 and nombres[1] == 0:
|
|
|
|
|
return False
|
|
|
|
|
if nombres[0] in (0, 127, 255) or nombres[3] == 0:
|
|
|
|
|
return False
|
|
|
|
|
if valeur.startswith(IPV4_DOCUMENTAIRES):
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def termes_interdits(chemin=NOMS_INTERDITS):
|
|
|
|
|
"""Les termes du fichier privé, en minuscules. Vide s'il n'existe pas."""
|
|
|
|
|
try:
|
|
|
|
|
with io.open(chemin, encoding="utf-8") as fh:
|
|
|
|
|
contenu = fh.read()
|
|
|
|
|
except OSError:
|
|
|
|
|
return []
|
|
|
|
|
termes = []
|
|
|
|
|
for ligne in contenu.split("\n"):
|
|
|
|
|
terme = ligne.strip()
|
|
|
|
|
if terme and not terme.startswith("#"):
|
|
|
|
|
termes.append(terme.lower())
|
|
|
|
|
return termes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def identifiants(texte, termes=()):
|
|
|
|
|
"""Les données identifiantes d'un texte : (motif, extrait, position).
|
|
|
|
|
|
|
|
|
|
Chaque OCCURRENCE est rendue, et non chaque valeur : la même adresse citée
|
|
|
|
|
à deux endroits est à corriger aux deux.
|
|
|
|
|
"""
|
|
|
|
|
trouves = []
|
|
|
|
|
|
|
|
|
|
for trouve in IPV4.finditer(texte):
|
|
|
|
|
if adresse_de_machine(trouve.group(0)):
|
|
|
|
|
trouves.append(("adresse", trouve.group(0), trouve.start()))
|
|
|
|
|
|
|
|
|
|
for trouve in EMAIL.finditer(texte):
|
|
|
|
|
if not EMAIL_PERMIS.search(trouve.group(0)):
|
|
|
|
|
trouves.append(("courriel", trouve.group(0), trouve.start()))
|
|
|
|
|
|
|
|
|
|
for trouve in HOME.finditer(texte):
|
|
|
|
|
if trouve.group(1) not in HOME_PERMIS:
|
|
|
|
|
trouves.append(
|
|
|
|
|
("compte", f"/home/{trouve.group(1)}/", trouve.start())
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
minuscules = texte.lower()
|
|
|
|
|
for terme in termes:
|
|
|
|
|
depart = minuscules.find(terme)
|
|
|
|
|
while depart != -1:
|
|
|
|
|
trouves.append(("nom privé", terme, depart))
|
|
|
|
|
depart = minuscules.find(terme, depart + 1)
|
|
|
|
|
|
|
|
|
|
return sorted(trouves, key=lambda t: t[2])
|