Compare commits
3 commits
6077b179af
...
34b7ec3886
| Author | SHA1 | Date | |
|---|---|---|---|
| 34b7ec3886 | |||
| 03c628d5e1 | |||
| 0eaceb1048 |
13 changed files with 979 additions and 39 deletions
|
|
@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
|
|||
## Écrire, puis relire (D-68)
|
||||
|
||||
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
||||
C'est aussi ce que font les 60 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
C'est aussi ce que font les 61 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
||||
dépôt annonce.**
|
||||
|
||||
|
|
|
|||
45
CHANGELOG.md
45
CHANGELOG.md
|
|
@ -1,5 +1,50 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-08 — La forme des registres cesse d'etre recopiee : elle est derivee
|
||||
|
||||
**61 preuves (P01-P61, dont une conditionnelle). `make prouver` : CONFORME, 60 OK, 0 echec,
|
||||
1 saute.**
|
||||
|
||||
### Le probleme
|
||||
|
||||
La forme du plan etait ecrite TROIS FOIS : dans les constantes du moteur (`ETATS_SERVEUR`,
|
||||
`PORTEES_BD`, `AUTORITES_DNS`), dans les formulaires du GUI, et dans la documentation. Rien
|
||||
ne les tenait ensemble. Ajouter une portee de base de donnees demandait trois gestes, et le
|
||||
troisieme s'oubliait sans qu'aucun controle ne s'en apercoive.
|
||||
|
||||
### Ce qui change
|
||||
|
||||
`scripts/schema_plan.py` (nouveau) derive un JSON Schema des SIX registres du plan
|
||||
— `serveurs`, `applications`, `bases_donnees`, `serveurs_bd`, `domaines_publics`,
|
||||
`nomenclature` — en IMPORTANT les enumerations du moteur plutot qu'en les recopiant. Le
|
||||
resultat est versionne dans `docs/audit/schema-plan.json` : 42 champs, regenerable par
|
||||
`make schema`. Il est versionne, et non recalcule a chaud, pour qu'une divergence se voie
|
||||
dans un diff.
|
||||
|
||||
Le schema porte ce que JSON Schema seul ne dit pas : `x-source-valeurs` (liste fermee
|
||||
alimentee a l'execution depuis l'inventaire), `x-source-selon` (liste dependant d'un autre
|
||||
champ), `x-clef` (l'attribut qui identifie l'entite), `x-requis`.
|
||||
|
||||
**Le formulaire des bases de donnees du GUI n'est plus ecrit a la main.** Il est construit
|
||||
au chargement depuis le schema servi par `/api/inventaire`. Le chemin de SAUVEGARDE aussi
|
||||
derive du schema : les champs ecrits sont ceux que le schema declare, avec ses valeurs par
|
||||
defaut — plus une liste de champs recopiee dans le JS.
|
||||
|
||||
`P61` garde l'ensemble : le schema doit couvrir tout ce que les plans REELS contiennent.
|
||||
Son controle negatif : retirer un champ du schema le fait echouer.
|
||||
|
||||
### La limite, dite franchement
|
||||
|
||||
**UN registre sur six est genere.** Les cinq autres formulaires restent ecrits a la main.
|
||||
|
||||
Et le trou connu reste ouvert : `couverture_gui.py verifier` echoue toujours sur
|
||||
`nomenclature.categorie` et `nomenclature.service` — deux champs presents dans les plans
|
||||
reels que le GUI ne sait pas ecrire. Le schema les DECRIT deja ; c'est le passage de la vue
|
||||
nomenclature au generateur qui fermera le trou, par construction.
|
||||
|
||||
Enfin : le formulaire genere a ete eprouve en rendant son HTML avec la vraie reponse de
|
||||
l'API, pas dans un navigateur.
|
||||
|
||||
## 2026-09-06 — Tournee des 74 documents : ce que le depot disait de lui-meme avait vieilli
|
||||
|
||||
**57 preuves (P01-P57, dont une conditionnelle). `make prouver` : CONFORME, 56 OK, 0 echec,
|
||||
|
|
|
|||
4
Makefile
4
Makefile
|
|
@ -797,6 +797,10 @@ flux-verifier: ## Verifie que le registre des flux correspond aux meta/flux.yml
|
|||
valider: ansible-runtime ## Passe la recette de validation sur la flotte
|
||||
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/valider.yml
|
||||
|
||||
.PHONY: schema
|
||||
schema: ## Regenere docs/audit/schema-plan.json depuis les registres et les validateurs
|
||||
python3 scripts/schema_plan.py
|
||||
|
||||
.PHONY: wiki-publier
|
||||
wiki-publier: ## Publie le wiki (wiki/) vers la forge
|
||||
@set -e; \
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (59 OK · 0 echec · 1 saute)
|
||||
- **Verdict** : ✅ CONFORME (60 OK · 0 echec · 1 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 34 VM placee(s), aucun nom ni VMID en collision. |
|
||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 32 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 21, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 57 scripts expliques et atteignables, 114 cibles make documentees, 65 roles avec README. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 58 scripts expliques et atteignables, 115 cibles make documentees, 65 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (131 cle(s) declaree(s) par l'instance). |
|
||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (37 groupes). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 43 document(s) declarent leur lecteur (34 genere(s) exempte(s)). |
|
||||
|
|
@ -53,14 +53,14 @@
|
|||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 39 role(s) serveur/client tous nommes, 40 groupe(s) cite(s) en table existent tous. |
|
||||
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. |
|
||||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 53 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 54 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 104 regle(s) du site. |
|
||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. |
|
||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 87 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 89 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (117 lignes). |
|
||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 169 regles `pass`), tous non consignes et tous motives. |
|
||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
||||
|
|
@ -69,10 +69,11 @@
|
|||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 10 role(s) applique(s), aucun secret de tenant reclame. |
|
||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. |
|
||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (60 preuves, 65 roles, 40 groupes). |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (61 preuves, 65 roles, 40 groupes). |
|
||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `e2935ed` (publie le 2026-09-08). |
|
||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `0eaceb1` (publie le 2026-09-08). |
|
||||
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 42 champ(s) sur 6 registres, et couvre tout ce que les plans reels contiennent ; applications:1, domaines_publics:1, nomenclature:9, serveurs:3 |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
|
|
|
|||
381
docs/audit/schema-plan.json
Normal file
381
docs/audit/schema-plan.json
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "GENERE par scripts/schema_plan.py (make schema). Ne pas editer a la main. Decrit la FORME des registres ; la COHERENCE reste aux validateurs de inventory_rules.py.",
|
||||
"registres": {
|
||||
"applications": {
|
||||
"entite": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"expose": {
|
||||
"description": "FQDN publies par l'edge. Derivent le vhost, les SAN et le plancher /etc/hosts.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Exposition publique",
|
||||
"type": "array"
|
||||
},
|
||||
"groupe": {
|
||||
"description": "La capacite appliquee. Doit avoir un playbook homonyme.",
|
||||
"title": "Groupe (rôle)",
|
||||
"type": "string",
|
||||
"x-source-valeurs": "groupes_operationnels"
|
||||
},
|
||||
"hote": {
|
||||
"description": "Une VM declaree au plan. Un hote inconnu est un « hote fantome » (P06).",
|
||||
"title": "Hôte",
|
||||
"type": "string",
|
||||
"x-source-valeurs": "serveurs"
|
||||
},
|
||||
"liens": {
|
||||
"description": "Roles acceptes par le role porteur (meta/liens.yml).",
|
||||
"items": {
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Liens (bindings)",
|
||||
"type": "array"
|
||||
},
|
||||
"port": {
|
||||
"title": "Port",
|
||||
"type": "integer"
|
||||
},
|
||||
"requiert": {
|
||||
"description": "Dependance applicative. Indicative : l'ordre de deploiement vient des couches.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Requiert",
|
||||
"type": "array"
|
||||
},
|
||||
"websocket": {
|
||||
"description": "L'edge doit relayer la mise a niveau de connexion.",
|
||||
"title": "WebSocket",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"groupe",
|
||||
"hote"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Applications",
|
||||
"x-clef": {
|
||||
"description": "Nom de l'application dans le plan.",
|
||||
"title": "Identifiant"
|
||||
},
|
||||
"x-fichier": "plan/applications.yml",
|
||||
"x-racine": "applications"
|
||||
},
|
||||
"bases_donnees": {
|
||||
"entite": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"base": {
|
||||
"title": "Base",
|
||||
"type": "string"
|
||||
},
|
||||
"consommateur": {
|
||||
"description": "Qui utilise cette base. La liste depend de la portee.",
|
||||
"title": "Consommateur",
|
||||
"type": "string",
|
||||
"x-source-selon": {
|
||||
"cas": {
|
||||
"application": "applications",
|
||||
"hote": "serveurs"
|
||||
},
|
||||
"champ": "portee",
|
||||
"defaut": "groupes_operationnels"
|
||||
}
|
||||
},
|
||||
"portee": {
|
||||
"default": "groupe",
|
||||
"description": "Comment le consommateur est designe : par application, par groupe ou par hote.",
|
||||
"enum": [
|
||||
"application",
|
||||
"groupe",
|
||||
"hote"
|
||||
],
|
||||
"title": "Portée",
|
||||
"type": "string"
|
||||
},
|
||||
"proprietaire": {
|
||||
"title": "Propriétaire",
|
||||
"type": "string"
|
||||
},
|
||||
"secret": {
|
||||
"description": "NOM d'une variable Vault, jamais une valeur. Le secret ne quitte pas le role.",
|
||||
"title": "Secret (Vault)",
|
||||
"type": "string"
|
||||
},
|
||||
"serveur": {
|
||||
"title": "Serveur de BD",
|
||||
"type": "string",
|
||||
"x-source-valeurs": "serveurs_bd"
|
||||
},
|
||||
"usage": {
|
||||
"default": "principale",
|
||||
"title": "Usage",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"base",
|
||||
"consommateur",
|
||||
"proprietaire",
|
||||
"secret",
|
||||
"serveur"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Bases de donnees",
|
||||
"x-clef": {
|
||||
"description": "Nom de l'entree au registre, ex. `bd_forgejo`.",
|
||||
"title": "Identifiant"
|
||||
},
|
||||
"x-fichier": "plan/bases-donnees.yml",
|
||||
"x-racine": "bases_donnees"
|
||||
},
|
||||
"domaines_publics": {
|
||||
"entite": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"autorite": {
|
||||
"enum": [
|
||||
"auto-heberge",
|
||||
"delegue",
|
||||
"primaire-cache"
|
||||
],
|
||||
"title": "Autorité DNS",
|
||||
"type": "string"
|
||||
},
|
||||
"dnssec": {
|
||||
"title": "DNSSEC",
|
||||
"type": "boolean"
|
||||
},
|
||||
"edge": {
|
||||
"title": "Edge",
|
||||
"type": "string",
|
||||
"x-source-valeurs": "serveurs"
|
||||
},
|
||||
"mail": {
|
||||
"description": "La zone porte des enregistrements de messagerie.",
|
||||
"title": "Courriel",
|
||||
"type": "boolean"
|
||||
},
|
||||
"secondaires": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Secondaires",
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Domaines publics",
|
||||
"x-clef": {
|
||||
"description": "Le nom public, ex. `chezlepro.ca`.",
|
||||
"title": "Domaine"
|
||||
},
|
||||
"x-fichier": "plan/domaines.yml",
|
||||
"x-racine": "domaines_publics"
|
||||
},
|
||||
"nomenclature": {
|
||||
"entite": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"categories": {
|
||||
"additionalProperties": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"libelle": {
|
||||
"description": "Nom lisible de la zone (Frontiere, Identite...).",
|
||||
"title": "Libellé",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"libelle"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"description": "Une zone de securite par cle. Le numero derive le 3e octet et le VLAN.",
|
||||
"title": "Catégories (zones)",
|
||||
"type": "object"
|
||||
},
|
||||
"cidr_hote": {
|
||||
"title": "CIDR d'hote",
|
||||
"type": "integer"
|
||||
},
|
||||
"fonctions": {
|
||||
"additionalProperties": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"categorie": {
|
||||
"description": "La zone. Fixe le 3e octet (15 + categorie) et le VLAN.",
|
||||
"title": "Catégorie",
|
||||
"type": "integer"
|
||||
},
|
||||
"service": {
|
||||
"description": "Fixe le bloc d'adresses de l'hote dans la zone.",
|
||||
"title": "Service",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"categorie",
|
||||
"service"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"description": "categorie + service par fonction. VMID et IP en derivent.",
|
||||
"title": "Fonctions",
|
||||
"type": "object"
|
||||
},
|
||||
"index": {
|
||||
"description": "LE seul champ d'adressage. Tout en derive ; P20 refuse d'en stocker un autre.",
|
||||
"maximum": 255,
|
||||
"minimum": 0,
|
||||
"title": "Index (seed)",
|
||||
"type": "integer"
|
||||
},
|
||||
"reservations": {
|
||||
"additionalProperties": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"passerelle": {
|
||||
"description": "Dernier octet de la passerelle. P23 refuse un SVI qui s'en ecarte.",
|
||||
"title": "Passerelle",
|
||||
"type": "integer"
|
||||
},
|
||||
"reserve_max": {
|
||||
"title": "Réserve (max)",
|
||||
"type": "integer"
|
||||
},
|
||||
"reserve_min": {
|
||||
"title": "Réserve (min)",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"description": "Adresses soustraites a la derivation dans chaque zone.",
|
||||
"title": "Réservations",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"index"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Nomenclature",
|
||||
"x-fichier": "plan/nomenclature.yml",
|
||||
"x-racine": null
|
||||
},
|
||||
"serveurs": {
|
||||
"entite": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"coeurs": {
|
||||
"description": "Vide = derive des empreintes des roles.",
|
||||
"title": "Cœurs",
|
||||
"type": "integer"
|
||||
},
|
||||
"disque": {
|
||||
"description": "Ex. `32G`. Vide = derive des empreintes des roles.",
|
||||
"title": "Disque",
|
||||
"type": "string"
|
||||
},
|
||||
"etat": {
|
||||
"description": "`planifie` = decrit mais pas deploye ; `actif` = joignable par Ansible.",
|
||||
"enum": [
|
||||
"actif",
|
||||
"planifie"
|
||||
],
|
||||
"title": "État",
|
||||
"type": "string"
|
||||
},
|
||||
"fonction": {
|
||||
"description": "Determine VMID, IP, VLAN et passerelle. Doit exister dans la nomenclature.",
|
||||
"title": "Fonction",
|
||||
"type": "string",
|
||||
"x-source-valeurs": "nomenclature.fonctions"
|
||||
},
|
||||
"integrations": {
|
||||
"description": "Seulement les facultatives. Les universelles viennent du role et sont refusees ici.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Intégrations facultatives",
|
||||
"type": "array"
|
||||
},
|
||||
"memoire": {
|
||||
"description": "Vide = derive des empreintes des roles.",
|
||||
"title": "Mémoire (Mo)",
|
||||
"type": "integer"
|
||||
},
|
||||
"noeud": {
|
||||
"description": "Surcharge le defaut de `make config`.",
|
||||
"title": "Nœud Proxmox",
|
||||
"type": "string"
|
||||
},
|
||||
"stockage": {
|
||||
"description": "Surcharge le defaut.",
|
||||
"title": "Stockage",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fonction"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Serveurs (VM)",
|
||||
"x-clef": {
|
||||
"description": "Le nom court de la VM. Le rang final derive l'adresse.",
|
||||
"title": "Nom d'hôte"
|
||||
},
|
||||
"x-fichier": "plan/serveurs.yml",
|
||||
"x-racine": "serveurs"
|
||||
},
|
||||
"serveurs_bd": {
|
||||
"entite": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"groupe": {
|
||||
"title": "Groupe",
|
||||
"type": "string",
|
||||
"x-source-valeurs": "groupes_operationnels"
|
||||
},
|
||||
"hote": {
|
||||
"title": "Hôte",
|
||||
"type": "string",
|
||||
"x-source-valeurs": "serveurs"
|
||||
},
|
||||
"port": {
|
||||
"title": "Port",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"title": "Moteur",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hote",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Serveurs de bases de donnees",
|
||||
"x-clef": {
|
||||
"description": "Nom du serveur de bases, ex. `pg-principal`.",
|
||||
"title": "Nom"
|
||||
},
|
||||
"x-fichier": "plan/bases-donnees.yml",
|
||||
"x-racine": "serveurs_bd"
|
||||
}
|
||||
},
|
||||
"title": "Registres du plan Set-OPS"
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.
|
||||
remote: ssh://git@eregion.chezlepro.ca:2222/Alliance-Boreale/Set-OPS-Public.wiki.git
|
||||
source: e2935ed
|
||||
source: 0eaceb1
|
||||
date: 2026-09-08
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ README de rôles). Cette page comble ces deux trous.
|
|||
| rôles | 65 | `roles/*/` |
|
||||
| README de rôles | 65 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
||||
| documents | 39 | `docs/*.md` |
|
||||
| pièces d'audit | 38 | `docs/audit/*` |
|
||||
| pièces d'audit | 39 | `docs/audit/*` |
|
||||
| unités de wiki | 27 | `wiki/*.md` |
|
||||
| décisions en vigueur | 81 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
|
||||
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
|
||||
|
|
@ -58,6 +58,7 @@ Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanis
|
|||
| Mécanisme | Ce que c'est | Où, dans le code | Doc |
|
||||
|---|---|---|---|
|
||||
| Plan → inventaire | `plan/*.yml` → `hosts.yml` généré | `scripts/instancier.py`, `scripts/inventory_rules.py` | `plan-et-generation.md` |
|
||||
| **Schéma des registres** | la **forme** des six registres — champs, types, énumérations, requis — **dérivée** des validateurs, jamais écrite à la main. Sert à générer les formulaires plutôt qu'à les écrire. La **cohérence** reste aux `valider_*` : un schéma ne sait pas dire qu'un `consommateur` désigne une application inexistante | `scripts/schema_plan.py` (`make schema`) → `docs/audit/schema-plan.json` ; garde **P61** | `plan-et-generation.md` |
|
||||
| Nomenclature dérivée | VMID / IP / VLAN / FQDN dérivés | `inventory_rules.deriver_nomenclature` + `plan/nomenclature.yml` | `nomenclature-vm.md` |
|
||||
| Dimensionnement | RAM/CPU/disque sommés par logiciel | `roles/*/meta/empreinte.yml` → `deriver_ressources` | `dimensionnement-ressources.md` |
|
||||
| **Bindings app→app** | lien **côté app** (`liens`) résolu en host_vars | `plan/applications.yml` `liens:` + `roles/*/meta/liens.yml` + `instancier.resoudre_liens` | `bindings-conception.md` |
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
|
|||
|
||||
## Le trou qu'il comble
|
||||
|
||||
`scripts/prouver.py` porte 60 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
`scripts/prouver.py` porte 61 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
||||
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||
propriétaire, que rien n'est codé en dur.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ main, et personne ne s'en apercoit avant de buter dessus.
|
|||
Ce script rend le trou VISIBLE et mesurable. Il croise :
|
||||
- les champs REELLEMENT presents dans les plans (instance courante + tous les
|
||||
modeles decouverts par scripts/modeles.py) ;
|
||||
- `CHAMPS_ECRITS_PAR_GUI` de scripts/inventory_gui.py, qui declare ce que les
|
||||
- `champs_ecrits_par_gui()` de scripts/inventory_gui.py, qui declare ce que les
|
||||
fonctions de sauvegarde ecrivent.
|
||||
|
||||
Deux sens de verification :
|
||||
|
|
@ -32,7 +32,7 @@ from pathlib import Path
|
|||
|
||||
import yaml
|
||||
|
||||
from inventory_gui import CHAMPS_ECRITS_PAR_GUI
|
||||
from inventory_gui import REGISTRES_GENERES, champs_ecrits_par_gui
|
||||
from modeles import decouvrir as decouvrir_modeles
|
||||
|
||||
from inventory_rules import instance_courante, instance_courante # noqa: E402
|
||||
|
|
@ -88,11 +88,18 @@ def declaration_derive() -> list[str]:
|
|||
"""Champs declares au GUI mais introuvables dans son source (declaration qui ment)."""
|
||||
src = SOURCE_GUI.read_text(encoding="utf-8")
|
||||
# On coupe le tableau lui-meme : sa presence ne prouve rien.
|
||||
debut = src.find("CHAMPS_ECRITS_PAR_GUI")
|
||||
debut = src.find("champs_ecrits_par_gui()")
|
||||
fin = src.find("\n}\n", debut)
|
||||
ailleurs = src[:debut] + src[fin:] if debut != -1 and fin != -1 else src
|
||||
absents = []
|
||||
for registre, champs in CHAMPS_ECRITS_PAR_GUI.items():
|
||||
for registre, champs in champs_ecrits_par_gui().items():
|
||||
# UN REGISTRE GENERE N'A PLUS SES CHAMPS DANS LE SOURCE, et c'est le but : ils
|
||||
# vivent dans le schema, et le formulaire en derive. Chercher `secret` dans le JS
|
||||
# ferait crier cette garde sur precisement le progres qu'elle devrait constater.
|
||||
# Pour ces registres-la, c'est P61 qui tient la promesse — elle verifie que le
|
||||
# schema decrit tout ce que les plans contiennent.
|
||||
if registre in REGISTRES_GENERES:
|
||||
continue
|
||||
for champ in sorted(champs):
|
||||
if champ not in ailleurs:
|
||||
absents.append(f"{registre}.{champ}")
|
||||
|
|
@ -112,7 +119,7 @@ def main() -> int:
|
|||
|
||||
if args.commande == "lister":
|
||||
for registre in sorted(utilises):
|
||||
couverts = CHAMPS_ECRITS_PAR_GUI.get(registre, set())
|
||||
couverts = champs_ecrits_par_gui().get(registre, set())
|
||||
print(f"\n== {registre} ==")
|
||||
for champ in sorted(utilises[registre]):
|
||||
etat = "GUI" if champ in couverts else "-- hors GUI --"
|
||||
|
|
@ -125,7 +132,7 @@ def main() -> int:
|
|||
for registre, champs in utilises.items():
|
||||
if registre in tolere:
|
||||
continue
|
||||
couverts = CHAMPS_ECRITS_PAR_GUI.get(registre, set())
|
||||
couverts = champs_ecrits_par_gui().get(registre, set())
|
||||
for champ in sorted(set(champs) - couverts):
|
||||
trous.append(f"{registre}.{champ} (vu dans : "
|
||||
f"{', '.join(sorted(champs[champ]))})")
|
||||
|
|
@ -138,7 +145,7 @@ def main() -> int:
|
|||
for t in trous:
|
||||
print(f" - {t}", file=sys.stderr)
|
||||
if derive:
|
||||
print(f"erreur: {len(derive)} champ(s) declare(s) dans CHAMPS_ECRITS_PAR_GUI mais "
|
||||
print(f"erreur: {len(derive)} champ(s) declare(s) dans champs_ecrits_par_gui() mais "
|
||||
f"introuvable(s) dans le source du GUI : {', '.join(derive)}", file=sys.stderr)
|
||||
|
||||
if trous or derive:
|
||||
|
|
|
|||
|
|
@ -464,9 +464,29 @@ def inventaire_api(path: Path) -> dict:
|
|||
"integrations_disponibles": integrations_disponibles(),
|
||||
"integrations_universelles": integrations_universelles(RACINE),
|
||||
"liens_acceptes": catalogue_liens(RACINE),
|
||||
# LE SCHEMA VOYAGE AVEC LES DONNEES. Le navigateur genere ses formulaires depuis
|
||||
# lui plutot que de les porter en dur : un champ ajoute au plan apparait a l'ecran
|
||||
# sans toucher au JS. Genere par `make schema`, garde par P61.
|
||||
"schema": schema_du_plan(),
|
||||
}
|
||||
|
||||
|
||||
def schema_du_plan() -> dict:
|
||||
"""Le JSON Schema des registres, tel que `make schema` l'a ecrit.
|
||||
|
||||
Lu depuis le fichier plutot que recalcule : c'est l'artefact VERSIONNE qui fait foi,
|
||||
et P61 refuse qu'il soit perime. Recalculer ici laisserait l'ecran afficher une forme
|
||||
que personne n'a relue.
|
||||
"""
|
||||
f = RACINE / "docs" / "audit" / "schema-plan.json"
|
||||
if not f.is_file():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(f.read_text(encoding="utf-8")).get("registres", {})
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def _lire_yaml_dict(path: Path) -> dict:
|
||||
if not path.exists():
|
||||
return {}
|
||||
|
|
@ -533,20 +553,46 @@ def intrants_courants() -> dict:
|
|||
# A TENIR A JOUR avec sauvegarderServeurs / sauvegarderApplications /
|
||||
# sauvegarderBases / sauvegarderDomaines. Le verificateur controle aussi que chaque
|
||||
# champ declare ici apparait bien dans le source du GUI (garde anti-derive).
|
||||
CHAMPS_ECRITS_PAR_GUI = {
|
||||
# CE QUE LE GUI SAIT ECRIRE — ET D'OU IL LE TIENT (2026-09-08).
|
||||
#
|
||||
# Cette table etait tenue A LA MAIN : quelqu'un devait penser a l'allonger quand un champ
|
||||
# arrivait au plan, et P19 confrontait la liste au reel. Une copie gardee reste une copie.
|
||||
#
|
||||
# `bases_donnees` en est SORTIE la premiere : son formulaire est desormais GENERE depuis
|
||||
# `docs/audit/schema-plan.json` (`make schema`), donc la liste s'en deduit au lieu d'etre
|
||||
# recopiee. Les registres restants gardent leur declaration manuelle jusqu'a ce que leur
|
||||
# formulaire passe au generateur — on ne bascule pas six vues d'un coup.
|
||||
#
|
||||
# P19 lit `champs_ecrits_par_gui()` et non plus cette constante : le jour ou la table sera
|
||||
# vide, elle gardera un mecanisme au lieu d'une liste.
|
||||
CHAMPS_ECRITS_A_LA_MAIN = {
|
||||
"serveurs": {"fonction", "etat", "noeud", "stockage", "disque", "memoire",
|
||||
"coeurs", "integrations"},
|
||||
"applications": {"groupe", "hote", "port", "requiert", "expose", "liens", "websocket"},
|
||||
"bases_donnees": {"serveur", "base", "proprietaire", "secret", "consommateur",
|
||||
"portee", "usage"},
|
||||
"serveurs_bd": {"type", "hote", "port", "groupe"},
|
||||
"domaines_publics": {"autorite", "edge", "secondaires", "dnssec", "mail"},
|
||||
# nomenclature : AUCUN champ. Le GUI la LIT (liste des fonctions) sans jamais
|
||||
# l'ecrire — ajouter une fonction exige d'editer le YAML. Trou connu et assume ;
|
||||
# `couverture_gui.py` le signale tant qu'il dure.
|
||||
# nomenclature : AUCUN champ ecrit a la main. Le GUI la LIT (liste des fonctions) sans
|
||||
# jamais l'ecrire — ajouter une fonction exige d'editer le YAML. Trou connu, que le
|
||||
# passage au generateur fermera par construction.
|
||||
"nomenclature": set(),
|
||||
}
|
||||
|
||||
# Les registres dont le formulaire est GENERE : leurs champs viennent du schema.
|
||||
REGISTRES_GENERES = {"bases_donnees"}
|
||||
|
||||
|
||||
def champs_ecrits_par_gui() -> dict:
|
||||
"""Ce que le GUI sait ecrire : declare a la main, plus derive du schema.
|
||||
|
||||
Une seule fonction repond a la question, pour que P19 n'ait pas a savoir lequel des
|
||||
deux mecanismes couvre quel registre.
|
||||
"""
|
||||
resultat = {nom: set(champs) for nom, champs in CHAMPS_ECRITS_A_LA_MAIN.items()}
|
||||
for nom, reg in (schema_du_plan() or {}).items():
|
||||
if nom in REGISTRES_GENERES:
|
||||
resultat[nom] = set((reg.get("entite") or {}).get("properties") or {})
|
||||
return resultat
|
||||
|
||||
|
||||
def integrations_disponibles() -> list:
|
||||
"""Roles client_* SELECTIONNABLES : ceux qui restent un choix.
|
||||
|
|
@ -1967,6 +2013,85 @@ HTML = r"""<!doctype html>
|
|||
applicationsModifie = false;
|
||||
}
|
||||
|
||||
// ---- FORMULAIRES GENERES DEPUIS LE SCHEMA -------------------------------------
|
||||
//
|
||||
// Le schema (docs/audit/schema-plan.json, `make schema`) decrit la FORME des
|
||||
// registres ; ces trois fonctions la rendent. Un champ ajoute au plan apparait donc
|
||||
// a l'ecran sans qu'on touche a ce fichier — c'est tout l'objet.
|
||||
//
|
||||
// Ce qu'elles ne font PAS : valider. La coherence reste aux `valider_*` du serveur,
|
||||
// qui refusent la sauvegarde. Un formulaire genere ne saurait pas dire qu'un
|
||||
// consommateur designe une application inexistante.
|
||||
|
||||
function sourceDeValeurs(nom) {
|
||||
// Les listes fermees, resolues a l'execution. C'est ce qui rend l'hote fantome
|
||||
// IMPOSSIBLE A SAISIR, au lieu d'etre refuse apres coup.
|
||||
if (nom === 'serveurs') return hotes.map(h => h.nom).filter(Boolean);
|
||||
if (nom === 'applications') return applications.map(x => x.id).filter(Boolean);
|
||||
if (nom === 'groupes_operationnels') return groupes;
|
||||
if (nom === 'serveurs_bd') return bdServeurs.map(x => x.nom).filter(Boolean);
|
||||
if (nom === 'nomenclature.fonctions') return Object.keys((data.nomenclature || {}).fonctions || {});
|
||||
return [];
|
||||
}
|
||||
|
||||
function champDepuisSchema(cle, spec, valeur, entite, surChangement) {
|
||||
const titre = echapper(spec.title || cle);
|
||||
const aide = spec.description ? ` title="${echapper(spec.description)}"` : '';
|
||||
// Marqueur discret : le schema SAIT ce qui est requis, le formulaire ecrit a la
|
||||
// main ne le montrait pas. Une asterisque plutot que le mot « requis », qui se
|
||||
// collait au libelle et se lisait « Base requis ».
|
||||
const req = (spec['x-requis'] ? '<abbr class="dsn" title="Champ requis" style="text-decoration:none"> *</abbr>' : '');
|
||||
const appel = (expr) => `${surChangement}('${cle}', ${expr})`;
|
||||
|
||||
// Liste fermee : soit fixe (enum), soit alimentee a l'execution, soit dependante
|
||||
// d'un autre champ de la meme entite.
|
||||
let options = null;
|
||||
if (spec.enum) options = spec.enum;
|
||||
else if (spec['x-source-valeurs']) options = sourceDeValeurs(spec['x-source-valeurs']);
|
||||
else if (spec['x-source-selon']) {
|
||||
const s = spec['x-source-selon'];
|
||||
const pivot = (entite || {})[s.champ];
|
||||
options = sourceDeValeurs((s.cas || {})[pivot] || s.defaut);
|
||||
}
|
||||
|
||||
if (options) {
|
||||
const opts = ['<option value="">—</option>'].concat(options.map(v =>
|
||||
`<option value="${echapper(v)}"${String(v) === String(valeur == null ? '' : valeur) ? ' selected' : ''}>${echapper(v)}</option>`)).join('');
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><select onchange="${appel('this.value')}">${opts}</select></label>`;
|
||||
}
|
||||
if (spec.type === 'boolean') {
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><input type="checkbox"${valeur ? ' checked' : ''} onchange="${appel('this.checked')}"></label>`;
|
||||
}
|
||||
if (spec.type === 'integer') {
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><input type="number" value="${echapper(valeur == null ? '' : valeur)}" oninput="${appel('this.value')}"></label>`;
|
||||
}
|
||||
if (spec.type === 'array') {
|
||||
// Une liste de textes se saisit separee par des virgules : le YAML reste une
|
||||
// liste, la saisie reste lisible.
|
||||
const v = Array.isArray(valeur) ? valeur.join(', ') : '';
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><input value="${echapper(v)}" placeholder="a, b" oninput="${appel('this.value.split(\',\').map(x=>x.trim()).filter(Boolean)')}"></label>`;
|
||||
}
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><input value="${echapper(valeur == null ? '' : valeur)}" oninput="${appel('this.value')}"></label>`;
|
||||
}
|
||||
|
||||
function formulaireDepuisSchema(nomRegistre, entite, surChangement, cleCourante, surChangementCle) {
|
||||
const reg = ((data.schema || {})[nomRegistre]) || null;
|
||||
if (!reg) return '<div class="vide">Aucun schéma pour ce registre — lancer <code>make schema</code>.</div>';
|
||||
const props = (reg.entite || {}).properties || {};
|
||||
const requis = new Set((reg.entite || {}).required || []);
|
||||
const morceaux = [];
|
||||
if (reg['x-clef'] && surChangementCle) {
|
||||
const k = reg['x-clef'];
|
||||
morceaux.push(`<label class="champ" title="${echapper(k.description || '')}"><span>${echapper(k.title)}</span>` +
|
||||
`<input value="${echapper(cleCourante == null ? '' : cleCourante)}" oninput="(${surChangementCle})(this.value)"></label>`);
|
||||
}
|
||||
Object.keys(props).forEach(cle => {
|
||||
const spec = Object.assign({}, props[cle], {'x-requis': requis.has(cle)});
|
||||
morceaux.push(champDepuisSchema(cle, spec, (entite || {})[cle], entite, surChangement));
|
||||
});
|
||||
return `<div class="grille">${morceaux.join('')}</div>`;
|
||||
}
|
||||
|
||||
function dsnBase(a) {
|
||||
const s = bdServeurs.find(x => x.nom === a.serveur);
|
||||
if (!s) return '(serveur inconnu : ' + echapper(a.serveur || '—') + ')';
|
||||
|
|
@ -2048,22 +2173,15 @@ HTML = r"""<!doctype html>
|
|||
} else {
|
||||
const a = bdApplis[info.i];
|
||||
if (!a) { cible.innerHTML = '<div class="detail"><div class="vide">Introuvable.</div></div>'; return; }
|
||||
const source = a.portee === 'hote' ? hotes.map(h => h.nom).filter(Boolean) : a.portee === 'application' ? applications.map(x => x.id) : groupes;
|
||||
const optConso = '<option value="">—</option>' + source.map(v => `<option value="${echapper(v)}" ${v === a.consommateur ? 'selected' : ''}>${echapper(v)}</option>`).join('');
|
||||
const optPortee = ['groupe', 'application', 'hote'].map(p => `<option value="${p}" ${p === (a.portee || 'groupe') ? 'selected' : ''}>${p}</option>`).join('');
|
||||
const optServeur = '<option value="">—</option>' + bdServeurs.map(x => `<option value="${echapper(x.nom)}" ${x.nom === a.serveur ? 'selected' : ''}>${echapper(x.nom)}</option>`).join('');
|
||||
// FORMULAIRE GENERE DEPUIS LE SCHEMA (2026-09-08) — plus aucun champ en dur.
|
||||
// Les huit `<label>` qui vivaient ici sont derives de docs/audit/schema-plan.json :
|
||||
// libelles, types, listes fermees, et la source CONDITIONNELLE du consommateur
|
||||
// (elle depend de la portee) que le schema declare desormais au lieu de la laisser
|
||||
// vivre dans ce fichier. Un champ ajoute au registre apparait sans toucher au JS,
|
||||
// et P61 refuse qu'un champ du plan manque au schema.
|
||||
cible.innerHTML = `<article class="detail">
|
||||
<div class="detail-tete"><div class="detail-nom"><span class="detail-nom-txt">${echapper(a.cle || '(sans id)')}</span></div><span class="grandit"></span><button type="button" class="danger" title="Retirer" onclick="retirerBaseBd()">✕</button></div>
|
||||
<div class="grille">
|
||||
<label class="champ"><span>Identifiant</span><input value="${echapper(a.cle)}" placeholder="bd_forgejo" oninput="definirBaseBd('cle', this.value)"></label>
|
||||
<label class="champ"><span>Portée</span><select onchange="definirBaseBd('portee', this.value)">${optPortee}</select></label>
|
||||
<label class="champ"><span>Consommateur</span><select onchange="definirBaseBd('consommateur', this.value)">${optConso}</select></label>
|
||||
<label class="champ"><span>Serveur</span><select onchange="definirBaseBd('serveur', this.value)">${optServeur}</select></label>
|
||||
<label class="champ"><span>Base</span><input value="${echapper(a.base)}" placeholder="forgejo" oninput="definirBaseBd('base', this.value)"></label>
|
||||
<label class="champ"><span>Propriétaire</span><input value="${echapper(a.proprietaire)}" oninput="definirBaseBd('proprietaire', this.value)"></label>
|
||||
<label class="champ"><span>Usage</span><input value="${echapper(a.usage)}" placeholder="principale" oninput="definirBaseBd('usage', this.value)"></label>
|
||||
<label class="champ"><span>Secret (Vault)</span><input value="${echapper(a.secret)}" placeholder="vault_bd_app" oninput="definirBaseBd('secret', this.value)"></label>
|
||||
</div>
|
||||
${formulaireDepuisSchema('bases_donnees', a, 'definirBaseBd', a.cle, "definirBaseBd.bind(null,'cle')")}
|
||||
<div class="dsn mono" style="margin-top:8px">${echapper(dsnBase(a))}</div>
|
||||
${(() => {
|
||||
const srv = bdServeurs.find(x => x.nom === a.serveur);
|
||||
|
|
@ -2080,7 +2198,20 @@ HTML = r"""<!doctype html>
|
|||
const sd = {};
|
||||
bdServeurs.forEach(s => { if (String(s.nom).trim()) sd[s.nom] = {type: s.type, hote: s.hote, port: parseInt(s.port, 10) || s.port, ...(s.groupe ? {groupe: s.groupe} : {})}; });
|
||||
const bd = {};
|
||||
bdApplis.forEach(a => { if (String(a.cle).trim()) bd[a.cle] = {serveur: a.serveur, base: a.base, proprietaire: a.proprietaire, secret: a.secret, consommateur: a.consommateur, portee: a.portee || 'groupe', usage: a.usage || 'principale'}; });
|
||||
// SAUVEGARDE DERIVEE DU SCHEMA (2026-09-08). Les sept champs etaient enumeres ici
|
||||
// en dur : un champ ajoute au registre serait apparu au formulaire genere et
|
||||
// aurait disparu SILENCIEUSEMENT a l'enregistrement — le pire des deux mondes.
|
||||
// Les valeurs par defaut viennent du schema (`default`), plus du JS.
|
||||
const propsBd = (((data.schema || {}).bases_donnees || {}).entite || {}).properties || {};
|
||||
bdApplis.forEach(a => {
|
||||
if (!String(a.cle).trim()) return;
|
||||
const e = {};
|
||||
Object.keys(propsBd).forEach(cle => {
|
||||
const v = a[cle];
|
||||
e[cle] = (v === undefined || v === '' || v === null) ? (propsBd[cle].default ?? v) : v;
|
||||
});
|
||||
bd[a.cle] = e;
|
||||
});
|
||||
const rep = await fetch('/api/bases', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json', 'X-Jeton': JETON}, body: JSON.stringify({serveurs_bd: sd, bases_donnees: bd})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -251,6 +251,85 @@ def _lignes_premiere_table(texte: str) -> int:
|
|||
return n
|
||||
|
||||
|
||||
def preuve_schema_du_plan() -> tuple[bool, str]:
|
||||
"""Le schema des registres decrit tout ce que les plans reels contiennent.
|
||||
|
||||
CE QU'IL REMPLACE (2026-09-08). Le GUI portait `CHAMPS_ECRITS_PAR_GUI`, une liste
|
||||
tenue A LA MAIN de ce qu'il savait ecrire, et P19 la confrontait au reel. C'etait une
|
||||
copie — gardee, donc honnete, mais une copie : quelqu'un devait penser a l'allonger.
|
||||
Le schema la remplace comme SOURCE : les formulaires en derivent, et cette preuve
|
||||
verifie que le schema, lui, n'a rien oublie.
|
||||
|
||||
DEUX SENS, ET LE SECOND EST LE MOINS EVIDENT :
|
||||
|
||||
1. tout champ present dans un plan reel (instance courante ET tous les modeles) est
|
||||
DECRIT par le schema — sinon l'operateur devra editer le YAML a la main, et le
|
||||
principe 10 se dement en silence ;
|
||||
2. tout champ decrit est REELLEMENT admis quelque part — un champ inventé dans le
|
||||
schema ferait apparaitre a l'ecran une case que rien ne consomme.
|
||||
|
||||
Le second sens n'est PAS une erreur en soi : `mail`, `coeurs`, `noeud` et `stockage`
|
||||
sont legitimes et simplement inutilises dans les plans d'aujourd'hui. On les COMPTE
|
||||
donc, sans refuser — la meme mesure que les lacunes nommees de P29. Refuser
|
||||
obligerait a retirer du schema un champ valide des que plus personne ne s'en sert.
|
||||
|
||||
Et le fichier genere doit etre a jour : un schema perime ferait generer des
|
||||
formulaires d'hier.
|
||||
"""
|
||||
import subprocess as _sp
|
||||
r = _sp.run([sys.executable, "scripts/schema_plan.py", "--verifier"],
|
||||
capture_output=True, text=True, cwd=str(RACINE))
|
||||
if r.returncode != 0:
|
||||
return False, (r.stderr or r.stdout).strip()
|
||||
|
||||
import json as _json
|
||||
schema = _json.loads((RACINE / "docs" / "audit" / "schema-plan.json").read_text(encoding="utf-8"))
|
||||
def _noms(props: dict) -> set:
|
||||
"""Tous les champs decrits, TABLES IMBRIQUEES COMPRISES.
|
||||
|
||||
`couverture_gui.champs_utilises()` aplatit : il descend dans
|
||||
`nomenclature.fonctions.*` et rend `categorie` et `service` comme s'ils etaient
|
||||
des champs de la nomenclature. Comparer a plat d'un cote et en arbre de l'autre
|
||||
faisait crier la preuve sur un schema correct — l'instrument mesurait autre chose
|
||||
que la cible. On aplatit donc des deux cotes.
|
||||
"""
|
||||
vus = set()
|
||||
for nom, p in (props or {}).items():
|
||||
vus.add(nom)
|
||||
sous = p.get("additionalProperties")
|
||||
if isinstance(sous, dict):
|
||||
vus |= _noms(sous.get("properties"))
|
||||
return vus
|
||||
|
||||
decrit = {nom: _noms(reg["entite"]["properties"])
|
||||
for nom, reg in schema["registres"].items()}
|
||||
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
import couverture_gui as _cg
|
||||
observes = {nom: set(champs) for nom, champs in _cg.champs_utilises().items()}
|
||||
|
||||
trous, inutilises = [], []
|
||||
for nom, champs in observes.items():
|
||||
manquants = sorted(champs - decrit.get(nom, set()))
|
||||
if manquants:
|
||||
trous.append(f"{nom} : {', '.join(manquants)} present(s) au plan, absent(s) du schema")
|
||||
for nom, champs in decrit.items():
|
||||
jamais = sorted(champs - observes.get(nom, set()))
|
||||
if jamais:
|
||||
inutilises.append(f"{nom}:{len(jamais)}")
|
||||
|
||||
if trous:
|
||||
return False, ("Le schema ne decrit pas tout ce que les plans contiennent :\n - "
|
||||
+ "\n - ".join(trous)
|
||||
+ "\n (ajouter le champ a REGISTRES dans scripts/schema_plan.py, "
|
||||
"puis `make schema`)")
|
||||
n = sum(len(c) for c in decrit.values())
|
||||
suffixe = (f" ; {', '.join(inutilises)} champ(s) decrits et inutilises dans les plans "
|
||||
f"d'aujourd'hui (legitime)") if inutilises else ""
|
||||
return True, (f"Le schema decrit {n} champ(s) sur {len(decrit)} registres, et couvre "
|
||||
f"tout ce que les plans reels contiennent{suffixe}.")
|
||||
|
||||
|
||||
def preuve_wiki_publie_a_jour() -> tuple[bool, str]:
|
||||
"""Le wiki publie sur la forge correspond a `wiki/` dans le depot.
|
||||
|
||||
|
|
@ -2758,6 +2837,8 @@ PREUVES: list[dict] = [
|
|||
"refs": [], "func": preuve_enumerations_annoncees},
|
||||
{"id": "P60", "titre": "Wiki publie : la forge sert ce que le depot dit",
|
||||
"refs": ["AFF-002"], "func": preuve_wiki_publie_a_jour},
|
||||
{"id": "P61", "titre": "Schema du plan : il decrit tout ce que les plans contiennent",
|
||||
"refs": ["AFF-033"], "func": preuve_schema_du_plan},
|
||||
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
|
||||
"func": preuve_devis_frontiere_du_site},
|
||||
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||
|
|
|
|||
289
scripts/schema_plan.py
Normal file
289
scripts/schema_plan.py
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
#!/usr/bin/env python3
|
||||
"""JSON Schema des registres du plan — DERIVE, jamais ecrit a la main.
|
||||
|
||||
CE QUE CE SCHEMA EST, ET CE QU'IL N'EST PAS.
|
||||
|
||||
Il decrit la **forme** des six registres du plan : quels champs existent, de quel type,
|
||||
lesquels sont requis, quelles valeurs sont admises. Il sert a **generer les formulaires**
|
||||
du GUI plutot qu'a les ecrire un par un.
|
||||
|
||||
Il ne remplace PAS les validateurs (`inventory_rules.valider_*`). Ceux-ci portent la
|
||||
**coherence** — un `consommateur` qui designe une application inexistante, une `fonction`
|
||||
absente de la nomenclature, une integration universelle recopiee dans le plan. Aucune de
|
||||
ces regles ne s'exprime en JSON Schema, et vouloir les y mettre creerait la seconde source
|
||||
de verite que tout ce depot refuse.
|
||||
|
||||
schema -> la FORME -> genere les champs du formulaire
|
||||
validateurs -> la COHERENCE -> refusent une saisie incoherente
|
||||
|
||||
D'OU VIENNENT LES VALEURS ADMISES. Elles sont **importees** des constantes que les
|
||||
validateurs appliquent (`ETATS_SERVEUR`, `PORTEES_BD`, `AUTORITES_DNS`), jamais recopiees.
|
||||
Une enumeration recopiee diverge : c'est la lecon des neuf resolutions d'instance que P41
|
||||
garde depuis.
|
||||
|
||||
CE QUI RESTE DECLARE ICI, ET POURQUOI. Le libelle, l'aide et le fait qu'un champ soit
|
||||
**derive** (donc non saisissable) ne se lisent nulle part ailleurs : ce sont des decisions
|
||||
d'interface. Elles vivent donc dans `REGISTRES` ci-dessous, a cote de la seule chose qui
|
||||
les justifie. La preuve **P61** confronte cette declaration aux champs REELLEMENT presents
|
||||
dans tous les plans et modeles : un champ oublie ici et utilise la-bas fait echouer le
|
||||
harnais.
|
||||
|
||||
Usage :
|
||||
python3 scripts/schema_plan.py # ecrit docs/audit/schema-plan.json
|
||||
python3 scripts/schema_plan.py --verifier # compare sans ecrire (code 1 si perime)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from inventory_rules import ( # noqa: E402
|
||||
AUTORITES_DNS,
|
||||
ETATS_SERVEUR,
|
||||
PORTEES_BD,
|
||||
ecriture_atomique,
|
||||
)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
CIBLE = RACINE / "docs" / "audit" / "schema-plan.json"
|
||||
|
||||
# `derive: true` = le moteur le calcule, l'interface l'AFFICHE sans le proposer a la
|
||||
# saisie. C'est la difference entre montrer et laisser modifier, et elle est structurante :
|
||||
# P20 refuse tout adressage stocke, donc un formulaire qui offrirait `vlan` ou `vmid`
|
||||
# inviterait a violer une regle que le harnais refuse ensuite.
|
||||
REGISTRES: dict = {
|
||||
"serveurs": {
|
||||
"titre": "Serveurs (VM)",
|
||||
"fichier": "plan/serveurs.yml",
|
||||
"racine": "serveurs",
|
||||
"clef": {"libelle": "Nom d'hôte", "aide": "Le nom court de la VM. Le rang final derive l'adresse."},
|
||||
"champs": {
|
||||
"fonction": {"type": "string", "requis": True,
|
||||
"libelle": "Fonction",
|
||||
"aide": "Determine VMID, IP, VLAN et passerelle. Doit exister dans la nomenclature.",
|
||||
"source_valeurs": "nomenclature.fonctions"},
|
||||
"etat": {"type": "string", "enum": sorted(ETATS_SERVEUR),
|
||||
"libelle": "État",
|
||||
"aide": "`planifie` = decrit mais pas deploye ; `actif` = joignable par Ansible."},
|
||||
"integrations": {"type": "array", "items": {"type": "string"},
|
||||
"libelle": "Intégrations facultatives",
|
||||
"aide": "Seulement les facultatives. Les universelles viennent du role et sont refusees ici."},
|
||||
"noeud": {"type": "string", "libelle": "Nœud Proxmox",
|
||||
"aide": "Surcharge le defaut de `make config`."},
|
||||
"stockage": {"type": "string", "libelle": "Stockage", "aide": "Surcharge le defaut."},
|
||||
"disque": {"type": "string", "libelle": "Disque",
|
||||
"aide": "Ex. `32G`. Vide = derive des empreintes des roles."},
|
||||
"memoire": {"type": "integer", "libelle": "Mémoire (Mo)",
|
||||
"aide": "Vide = derive des empreintes des roles."},
|
||||
"coeurs": {"type": "integer", "libelle": "Cœurs",
|
||||
"aide": "Vide = derive des empreintes des roles."},
|
||||
},
|
||||
},
|
||||
"applications": {
|
||||
"titre": "Applications",
|
||||
"fichier": "plan/applications.yml",
|
||||
"racine": "applications",
|
||||
"clef": {"libelle": "Identifiant", "aide": "Nom de l'application dans le plan."},
|
||||
"champs": {
|
||||
"groupe": {"type": "string", "requis": True, "libelle": "Groupe (rôle)",
|
||||
"aide": "La capacite appliquee. Doit avoir un playbook homonyme.",
|
||||
"source_valeurs": "groupes_operationnels"},
|
||||
"hote": {"type": "string", "requis": True, "libelle": "Hôte",
|
||||
"aide": "Une VM declaree au plan. Un hote inconnu est un « hote fantome » (P06).",
|
||||
"source_valeurs": "serveurs"},
|
||||
"port": {"type": "integer", "libelle": "Port"},
|
||||
"expose": {"type": "array", "items": {"type": "string"},
|
||||
"libelle": "Exposition publique",
|
||||
"aide": "FQDN publies par l'edge. Derivent le vhost, les SAN et le plancher /etc/hosts."},
|
||||
"requiert": {"type": "array", "items": {"type": "string"},
|
||||
"libelle": "Requiert",
|
||||
"aide": "Dependance applicative. Indicative : l'ordre de deploiement vient des couches."},
|
||||
"liens": {"type": "array", "items": {"type": "object"},
|
||||
"libelle": "Liens (bindings)",
|
||||
"aide": "Roles acceptes par le role porteur (meta/liens.yml)."},
|
||||
"websocket": {"type": "boolean", "libelle": "WebSocket",
|
||||
"aide": "L'edge doit relayer la mise a niveau de connexion."},
|
||||
},
|
||||
},
|
||||
"bases_donnees": {
|
||||
"titre": "Bases de donnees",
|
||||
"fichier": "plan/bases-donnees.yml",
|
||||
"racine": "bases_donnees",
|
||||
"clef": {"libelle": "Identifiant", "aide": "Nom de l'entree au registre, ex. `bd_forgejo`."},
|
||||
"champs": {
|
||||
"serveur": {"type": "string", "requis": True, "libelle": "Serveur de BD",
|
||||
"source_valeurs": "serveurs_bd"},
|
||||
"base": {"type": "string", "requis": True, "libelle": "Base"},
|
||||
"proprietaire": {"type": "string", "requis": True, "libelle": "Propriétaire"},
|
||||
"secret": {"type": "string", "requis": True, "libelle": "Secret (Vault)",
|
||||
"aide": "NOM d'une variable Vault, jamais une valeur. Le secret ne quitte pas le role."},
|
||||
# SOURCE CONDITIONNELLE : ce qu'on peut consommer depend de la PORTEE.
|
||||
# Le formulaire ecrit a la main faisait deja ce choix ; le schema le declare
|
||||
# au lieu de le laisser vivre dans le JS. C'est ce qui permet de generer le
|
||||
# champ sans perdre la regle.
|
||||
"consommateur": {"type": "string", "requis": True, "libelle": "Consommateur",
|
||||
"aide": "Qui utilise cette base. La liste depend de la portee.",
|
||||
"source_selon": {"champ": "portee",
|
||||
"cas": {"hote": "serveurs",
|
||||
"application": "applications"},
|
||||
"defaut": "groupes_operationnels"}},
|
||||
"portee": {"type": "string", "enum": sorted(PORTEES_BD), "libelle": "Portée",
|
||||
"defaut": "groupe",
|
||||
"aide": "Comment le consommateur est designe : par application, par groupe ou par hote."},
|
||||
"usage": {"type": "string", "libelle": "Usage", "defaut": "principale"},
|
||||
},
|
||||
},
|
||||
"serveurs_bd": {
|
||||
"titre": "Serveurs de bases de donnees",
|
||||
"fichier": "plan/bases-donnees.yml",
|
||||
"racine": "serveurs_bd",
|
||||
"clef": {"libelle": "Nom", "aide": "Nom du serveur de bases, ex. `pg-principal`."},
|
||||
"champs": {
|
||||
"type": {"type": "string", "requis": True, "libelle": "Moteur"},
|
||||
"hote": {"type": "string", "requis": True, "libelle": "Hôte", "source_valeurs": "serveurs"},
|
||||
"port": {"type": "integer", "libelle": "Port"},
|
||||
"groupe": {"type": "string", "libelle": "Groupe", "source_valeurs": "groupes_operationnels"},
|
||||
},
|
||||
},
|
||||
"domaines_publics": {
|
||||
"titre": "Domaines publics",
|
||||
"fichier": "plan/domaines.yml",
|
||||
"racine": "domaines_publics",
|
||||
"clef": {"libelle": "Domaine", "aide": "Le nom public, ex. `chezlepro.ca`."},
|
||||
"champs": {
|
||||
"autorite": {"type": "string", "enum": sorted(AUTORITES_DNS), "libelle": "Autorité DNS"},
|
||||
"edge": {"type": "string", "libelle": "Edge", "source_valeurs": "serveurs"},
|
||||
"secondaires": {"type": "array", "items": {"type": "string"}, "libelle": "Secondaires"},
|
||||
"dnssec": {"type": "boolean", "libelle": "DNSSEC"},
|
||||
"mail": {"type": "boolean", "libelle": "Courriel",
|
||||
"aide": "La zone porte des enregistrements de messagerie."},
|
||||
},
|
||||
},
|
||||
"nomenclature": {
|
||||
"titre": "Nomenclature",
|
||||
"fichier": "plan/nomenclature.yml",
|
||||
"racine": None, # pas d'entites : des clefs a la racine
|
||||
"champs": {
|
||||
"index": {"type": "integer", "requis": True, "libelle": "Index (seed)",
|
||||
"minimum": 0, "maximum": 255,
|
||||
"aide": "LE seul champ d'adressage. Tout en derive ; P20 refuse d'en stocker un autre."},
|
||||
"cidr_hote": {"type": "integer", "libelle": "CIDR d'hote"},
|
||||
# LES TROIS TABLES IMBRIQUEES SONT DECRITES, PAS RESUMEES EN « object ».
|
||||
# C'est P61 qui l'a impose : `categorie` et `service` etaient presents dans
|
||||
# tous les plans et absents du schema — donc un formulaire genere n'aurait
|
||||
# jamais su les editer, exactement le trou que la nomenclature avait deja
|
||||
# dans `CHAMPS_ECRITS_PAR_GUI` (zero champ declare).
|
||||
"categories": {"type": "object", "libelle": "Catégories (zones)",
|
||||
"aide": "Une zone de securite par cle. Le numero derive le 3e octet et le VLAN.",
|
||||
"entree": {
|
||||
"libelle": {"type": "string", "requis": True,
|
||||
"libelle": "Libellé",
|
||||
"aide": "Nom lisible de la zone (Frontiere, Identite...)."}}},
|
||||
"fonctions": {"type": "object", "libelle": "Fonctions",
|
||||
"aide": "categorie + service par fonction. VMID et IP en derivent.",
|
||||
"entree": {
|
||||
"categorie": {"type": "integer", "requis": True,
|
||||
"libelle": "Catégorie",
|
||||
"aide": "La zone. Fixe le 3e octet (15 + categorie) et le VLAN."},
|
||||
"service": {"type": "integer", "requis": True,
|
||||
"libelle": "Service",
|
||||
"aide": "Fixe le bloc d'adresses de l'hote dans la zone."}}},
|
||||
"reservations": {"type": "object", "libelle": "Réservations",
|
||||
"aide": "Adresses soustraites a la derivation dans chaque zone.",
|
||||
"entree": {
|
||||
"passerelle": {"type": "integer", "libelle": "Passerelle",
|
||||
"aide": "Dernier octet de la passerelle. P23 refuse un SVI qui s'en ecarte."},
|
||||
"reserve_min": {"type": "integer", "libelle": "Réserve (min)"},
|
||||
"reserve_max": {"type": "integer", "libelle": "Réserve (max)"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def construire() -> dict:
|
||||
"""Le JSON Schema, derive de REGISTRES et des constantes des validateurs."""
|
||||
defs = {}
|
||||
for nom, reg in REGISTRES.items():
|
||||
props, requis = {}, []
|
||||
for champ, d in reg["champs"].items():
|
||||
p = {"type": d["type"], "title": d["libelle"]}
|
||||
for cle_src, cle_dst in (("aide", "description"), ("enum", "enum"),
|
||||
("items", "items"), ("minimum", "minimum"),
|
||||
("maximum", "maximum"), ("defaut", "default")):
|
||||
if cle_src in d:
|
||||
p[cle_dst] = d[cle_src]
|
||||
if "entree" in d:
|
||||
# Une table dont chaque VALEUR a une forme connue : le formulaire genere
|
||||
# une sous-fiche par cle, au lieu d'offrir un bloc YAML libre.
|
||||
sous, sous_requis = {}, []
|
||||
for sc, sd in d["entree"].items():
|
||||
q = {"type": sd["type"], "title": sd["libelle"]}
|
||||
if "aide" in sd:
|
||||
q["description"] = sd["aide"]
|
||||
sous[sc] = q
|
||||
if sd.get("requis"):
|
||||
sous_requis.append(sc)
|
||||
p["additionalProperties"] = {"type": "object", "properties": sous,
|
||||
"additionalProperties": False}
|
||||
if sous_requis:
|
||||
p["additionalProperties"]["required"] = sorted(sous_requis)
|
||||
if "source_selon" in d:
|
||||
# Meme intention que `x-source-valeurs`, mais la liste change selon la
|
||||
# valeur d'un autre champ de la meme entite.
|
||||
p["x-source-selon"] = d["source_selon"]
|
||||
if "source_valeurs" in d:
|
||||
# Extension hors JSON Schema : dit au formulaire d'offrir une LISTE
|
||||
# fermee, alimentee a l'execution. C'est ce qui rend l'hote fantome
|
||||
# impossible a saisir plutot que refuse apres coup.
|
||||
p["x-source-valeurs"] = d["source_valeurs"]
|
||||
props[champ] = p
|
||||
if d.get("requis"):
|
||||
requis.append(champ)
|
||||
entite = {"type": "object", "properties": props,
|
||||
"additionalProperties": False}
|
||||
if requis:
|
||||
entite["required"] = sorted(requis)
|
||||
defs[nom] = {"title": reg["titre"], "x-fichier": reg["fichier"],
|
||||
"x-racine": reg["racine"], "entite": entite}
|
||||
if reg.get("clef"):
|
||||
# La CLEF n'est pas une propriete de l'entite — c'est son nom dans la table.
|
||||
# Le formulaire doit pourtant l'offrir : sans elle, on ne peut pas creer.
|
||||
defs[nom]["x-clef"] = {"title": reg["clef"]["libelle"],
|
||||
"description": reg["clef"]["aide"]}
|
||||
return {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Registres du plan Set-OPS",
|
||||
"description": ("GENERE par scripts/schema_plan.py (make schema). Ne pas editer a "
|
||||
"la main. Decrit la FORME des registres ; la COHERENCE reste aux "
|
||||
"validateurs de inventory_rules.py."),
|
||||
"registres": defs,
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--verifier", action="store_true",
|
||||
help="compare sans ecrire ; code 1 si le fichier est perime")
|
||||
args = ap.parse_args(argv)
|
||||
rendu = json.dumps(construire(), ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
||||
if args.verifier:
|
||||
actuel = CIBLE.read_text(encoding="utf-8") if CIBLE.is_file() else ""
|
||||
if actuel != rendu:
|
||||
print(f"erreur: {CIBLE.relative_to(RACINE)} est PERIME. Regenerer : make schema",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
n = sum(len(r["champs"]) for r in REGISTRES.values())
|
||||
print(f"CONFORME : {len(REGISTRES)} registres, {n} champs decrits.")
|
||||
return 0
|
||||
with ecriture_atomique(CIBLE) as f:
|
||||
f.write(rendu)
|
||||
print(f"Ecrit : {CIBLE.relative_to(RACINE)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -26,7 +26,7 @@ Trois idées la portent :
|
|||
|
||||
- **Le registre** : `docs/audit/affirmations.md` — chaque affirmation du dépôt (README, docs,
|
||||
aide `make`, GUI) reliée à une preuve et un statut (✅/🟡/❌/⚪).
|
||||
- **Le harnais** : `make prouver` rejoue les preuves automatisables (**P01–P60**, sans trou dans la série) et écrit
|
||||
- **Le harnais** : `make prouver` rejoue les preuves automatisables (**P01–P61**, sans trou dans la série) et écrit
|
||||
`docs/audit/preuve-<date>.md`. `make verifier` les inclut : il **échoue** si une preuve échoue.
|
||||
- **Chaque preuve garde une classe d'erreur.** Extrait :
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue