[DTP-Worker] Sprint 2 · Agrégateur RBAC : run-book d'application VPS unifié (3 volets → 1 plan ordonné SPEC §7)
Clôt le volet RBAC en-repo : recoud fixtures Role+DocPerm, plan User Permission et Role Profile en un run-book ordonné + manifeste agrégé. Zéro chiffre recalculé (tout vient d'un manifeste source, #6), graphe de dépendances validé (Role avant Role Profile), cohérence inter-volets + couverture bijective 50/50. 16 tests + job CI rbac-applyplan-tests · 99 tests de régression au total. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+19
-1
@@ -143,10 +143,28 @@ jobs:
|
||||
- name: Tests générateur de Role Profile
|
||||
run: python3 -m unittest discover -s tests -v
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Agrégateur RBAC (Sprint 2 · ERPNext Backend) : recoud les 3 volets RBAC
|
||||
# (fixtures Role+DocPerm, plan User Permission, Role Profile) en un run-book
|
||||
# d'application VPS ordonné (SPEC §7) + un manifeste agrégé. Valide contre
|
||||
# apply_plan.schema.json + invariants (ordre + graphe de dépendances SPEC §7,
|
||||
# cohérence inter-volets, couverture bijective des 50 rôles). stdlib pur.
|
||||
# --------------------------------------------------------------------------
|
||||
rbac-applyplan-tests:
|
||||
name: RBAC · run-book d'application unifié (agrégat 3 volets)
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: 05_deliverables_mvp/rbac/apply_plan
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Tests agrégateur RBAC
|
||||
run: python3 -m unittest discover -s tests -v
|
||||
|
||||
gate:
|
||||
name: Gate qualité (agrégat)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [constraints-guard, validate-json, check-docs, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests]
|
||||
needs: [constraints-guard, validate-json, check-docs, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests, rbac-applyplan-tests]
|
||||
steps:
|
||||
- name: Résultat
|
||||
run: echo "✅ Gate qualité 4Big franchi — tous les checks verts."
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Daily Report · 2026-07-30 · Claude Code DTP Worker (session 9)
|
||||
|
||||
**Session** : `20260730_042654`
|
||||
|
||||
## Tâche exécutée
|
||||
**Sprint 2 · Livrable ERPNext Backend « Agrégateur RBAC : run-book d'application
|
||||
VPS unifié (3 volets → 1 plan ordonné) »**
|
||||
(roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md` §Sprint 2 « RBAC 50 rôles » ·
|
||||
`RBAC_50_ROLES_SPEC.md` §7 · **prochaine tâche suggérée** par le daily report
|
||||
session 8 : « agrégateur MANIFEST unifié Role + DocPerm + User Permission +
|
||||
Role Profile pour clore le volet RBAC »).
|
||||
|
||||
## Contexte / analyse
|
||||
- Relu `CLAUDE.md`, `ROADMAP_8_WEEKS_OR_LESS.md`, daily reports sessions 7 et 8,
|
||||
`rbac_50_roles.json`, les **trois générateurs RBAC** livrés (`fixtures_gen`,
|
||||
`userperm_gen`, `roleprofile_gen`) + leurs manifestes, le validateur maison
|
||||
Publiciste, le patron CI, la SPEC §7.
|
||||
- État Sprint 2 : contrat RBAC (s4), Publiciste (s3), Faisabilité (s5), fixtures
|
||||
`Role`+`Custom DocPerm` (s6), plan `User Permission` (s7), `Role Profile` (s8)
|
||||
livrés. Frontend/CRM dépendent de layouts LIVE (VPS) → hors périmètre worker.
|
||||
- **Priorité évidente** (report s8 + SPEC §7) : les 3 volets existent mais l'ordre
|
||||
d'application restait **implicite**. Or il porte des contraintes ERPNext dures
|
||||
(Role importé AVANT Role Profile) et des confirmations préalables dispersées
|
||||
dans 3 manifestes. Le maillon manquant = la **vue d'ensemble** : un run-book
|
||||
ordonné + un manifeste agrégé. 100 % autorable sans VPS/pip/API, même patron
|
||||
que les 3 générateurs, clôt le volet RBAC en-repo.
|
||||
|
||||
## Réalisé — module `05_deliverables_mvp/rbac/apply_plan/`
|
||||
- `applylib/aggregator.py` — **agrège sans rien recalculer** : appelle les 3
|
||||
builders (`fixtures_builder.build_bundle`, `userperm_builder.build_plan`,
|
||||
`roleprofile_builder.build_bundle`), recoupe la cohérence (même contrat / même
|
||||
version / même cible 50 → `_require_same` lève sur mélange), fusionne les
|
||||
confirmations VPS, et produit le **run-book des 6 étapes SPEC §7** (responsable
|
||||
worker/vps, statut, commande worker, artefacts, `depends_on`, confirmations,
|
||||
lien doc). Ordre + dépendances = savoir procédural documenté, pas des chiffres.
|
||||
- `rbac_apply_plan.py` — CLI `build` / `validate`. **Refuse d'écrire** si un
|
||||
invariant casse : couverture SPEC §7 exacte + `order` séquentiel, graphe de
|
||||
dépendances (aucune arête en avant, aucun renvoi fantôme), Role Profile **après**
|
||||
fixtures Role, confirmations ni orphelines ni fantômes, cohérence inter-volets,
|
||||
**couverture bijective** (Role == 50 == entrées UP == rôles couverts profils),
|
||||
listes triées.
|
||||
- `apply_plan.schema.json` — contrat de sortie draft-07 (sous-ensemble supporté
|
||||
par le **validateur maison Publiciste**, zéro pip · `commande` nullable via
|
||||
type-array `["string","null"]`).
|
||||
- `tests/test_apply_plan.py` — **16 tests `unittest` (stdlib pur)** : schéma
|
||||
(maison + oracle `jsonschema`), fidélité des comptes aux manifestes source,
|
||||
fusion des confirmations, ordre + graphe SPEC §7, Role avant Role Profile,
|
||||
garde-fous de cohérence (rejet version/cible discordantes), déterminisme,
|
||||
détection d'invariants cassés.
|
||||
- **CI** : job `rbac-applyplan-tests` ajouté au **gate** de
|
||||
`.gitea/workflows/ci.yml` (Gitea Actions uniquement · #2).
|
||||
- `README.md` + `.gitignore` (output `out/` non commité, re-généré à la demande).
|
||||
- **Docs cousues** : `RBAC_50_ROLES_SPEC.md §7` (préambule pointe le run-book
|
||||
consolidé + nouveau ✅ encart livré).
|
||||
|
||||
## Anti-invention (#6) appliqué
|
||||
- **Zéro chiffre recalculé** : chaque compte du manifeste agrégé provient du
|
||||
manifeste d'un builder source (vérifié par `test_counts_come_from_source_manifests`).
|
||||
- Chaque item « à confirmer VPS » (DocType custom / Company / rôles `equipe`) est
|
||||
**repris tel quel** des manifestes d'origine, juste trié et fusionné.
|
||||
- L'ordre des étapes et le graphe de dépendances encodent des **contraintes
|
||||
ERPNext v15 documentées** (SPEC §7), pas des décisions fabriquées.
|
||||
- Un mélange de volets (versions de contrat différentes) est **refusé**, jamais
|
||||
silencieusement concilié.
|
||||
|
||||
## Vérifications effectuées (en-repo, sans toucher au VPS)
|
||||
- **16/16 tests verts** (schéma maison + oracle `jsonschema`). Génération réelle :
|
||||
**6 étapes**, comptes agrégés 50 rôles / 116 DocPerm / 45 DocTypes / 28 UP
|
||||
templates / 6 Role Profile ; confirmations VPS = 4 DocType custom + 5 Company +
|
||||
4 rôles `equipe` ; **couverture bijective 50/50 recoupée sur les 3 volets**.
|
||||
- **Gate CI local vert (exit 0)** : `guard_constraints.sh`, `validate_json.sh`,
|
||||
`check_docs.sh` (0 lien cassé, mention score présente), YAML `ci.yml` valide.
|
||||
- **Régression** : 23 Publiciste + 10 RBAC + 11 fixtures + 12 userperm + 11
|
||||
roleprofile + 16 apply_plan + 16 Faisabilité = **99 tests verts**.
|
||||
|
||||
## Non fait (hors périmètre worker · VPS)
|
||||
- Exécution réelle du run-book (`bench migrate`, création DocTypes custom,
|
||||
affectation `role_profile_name`, matérialisation `User Permission` par
|
||||
utilisateur) → agent ERPNext Backend (SPEC §7 ét. 2/3/4/5/6).
|
||||
|
||||
## Prochaine tâche suggérée
|
||||
- **Volet RBAC clos en-repo** (4 générateurs + run-book unifié). Suite logique :
|
||||
Faisabilité S3 (`40_llm_outputs/` + rapports bancables FR/EN/ES) ; ou préparer
|
||||
le hand-off ERPNext (packaging `fixtures/` de l'app OTO à partir des 4 sorties).
|
||||
- Frontend/CRM S2 : dépend des layouts LIVE (VPS) → hors périmètre worker.
|
||||
|
||||
---
|
||||
|
||||
**Auto-score 4Big du livrable Agrégateur RBAC (run-book) : 96/100.**
|
||||
Réserve −4 : l'exécution réelle du run-book reste côté VPS (agent ERPNext, hors
|
||||
périmètre worker, #8). Validé statiquement en-repo (16 tests verts + schéma
|
||||
conforme + recoupement bijectif des 3 volets + gate CI vert · 99 tests de
|
||||
régression au total).
|
||||
@@ -122,7 +122,11 @@ du code sans vérifier l'existence courante »).
|
||||
## 7. Application sur VPS (hors périmètre worker)
|
||||
|
||||
Ce worker n'écrit **jamais** sur le VPS. L'agent ERPNext Backend appliquera la
|
||||
cible côté serveur (`erpnext-backend-1`) :
|
||||
cible côté serveur (`erpnext-backend-1`) dans l'ordre ci-dessous. Ce run-book
|
||||
est **consolidé et vérifié** en-repo par l'agrégateur
|
||||
[`apply_plan/`](apply_plan/README.md) (`python3 rbac_apply_plan.py build` →
|
||||
`apply_plan.json` + `MANIFEST.json` agrégé : comptes des 3 volets, confirmations
|
||||
VPS fusionnées, graphe de dépendances, recoupement bijectif des 50 rôles) :
|
||||
|
||||
1. Générer les fixtures `Role` + `Custom DocPerm` depuis `rbac_50_roles.json`
|
||||
— **livré** : [`fixtures_gen/`](fixtures_gen/README.md) (`python3 rbac_fixtures_gen.py build`).
|
||||
@@ -159,6 +163,13 @@ cible côté serveur (`erpnext-backend-1`) :
|
||||
> métier + console technique `plateforme`) couvrant les **50 rôles de façon
|
||||
> bijective**, 100 % DocTypes natifs v15 (`Role Profile` + `Has Role`), aucun
|
||||
> DocType custom à confirmer. 11 tests + job CI `rbac-roleprofile-tests`.
|
||||
>
|
||||
> ✅ Le **run-book d'application unifié** (agrégat des 3 volets ci-dessus) est
|
||||
> **livré** en-repo ([`apply_plan/`](apply_plan/README.md)) : les 6 étapes de ce
|
||||
> §7 sous forme d'un plan ordonné + un manifeste agrégé (comptes consolidés,
|
||||
> confirmations VPS fusionnées, cohérence inter-volets). Zéro chiffre recalculé
|
||||
> (tout vient d'un manifeste source, #6), graphe de dépendances validé (Role
|
||||
> avant Role Profile). 16 tests + job CI `rbac-applyplan-tests`.
|
||||
|
||||
## 8. Vérification (en-repo, sans toucher au VPS)
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Sortie re-générable à la demande (déterministe) — non commitée.
|
||||
out/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -0,0 +1,95 @@
|
||||
# Agrégateur RBAC · run-book d'application VPS unifié
|
||||
|
||||
**Sprint 2 · agent ERPNext Backend.** Quatrième et dernier maillon RBAC en-repo :
|
||||
la **vue d'ensemble**. Les trois générateurs livrés produisent chacun une pièce
|
||||
isolée du puzzle ; cet agrégateur les recoud en **un seul plan d'application
|
||||
ordonné** (SPEC §7) que l'agent ERPNext suit pas à pas sur le VPS.
|
||||
|
||||
| Générateur | Répond à | Produit |
|
||||
|---|---|---|
|
||||
| [`fixtures_gen`](../fixtures_gen/README.md) | quels verbes / quel DocType | `Role` + `Custom DocPerm` |
|
||||
| [`userperm_gen`](../userperm_gen/README.md) | sur quelles lignes | plan `User Permission` row-level |
|
||||
| [`roleprofile_gen`](../roleprofile_gen/README.md) | quel bundle assignable | `Role Profile` par portail |
|
||||
| **`apply_plan`** (ce module) | **dans quel ordre appliquer** | **run-book + manifeste agrégé** |
|
||||
|
||||
> Ce worker **n'écrit jamais sur le VPS** (contrainte #8). Il produit un plan
|
||||
> ordonné en-repo ; l'application réelle (`bench migrate`, affectations) reste
|
||||
> côté serveur.
|
||||
|
||||
## Pourquoi un agrégateur (et pas juste trois modules)
|
||||
|
||||
Trois sorties séparées laissent l'ordre d'application implicite. Or cet ordre
|
||||
porte des **contraintes ERPNext dures** : par exemple, les lignes `Has Role` d'un
|
||||
`Role Profile` référencent des `Role` qui doivent **déjà exister** → il faut
|
||||
importer les fixtures `Role` **avant** les `Role Profile`. Certaines étapes
|
||||
exigent aussi des **confirmations préalables** (DocTypes custom, Companies, rôles
|
||||
de portée `equipe`). L'agrégateur matérialise ce **graphe de dépendances** et
|
||||
**recoupe la cohérence** des trois volets (même contrat, même cible 50 rôles,
|
||||
couverture bijective) — un seul artefact à lire pour l'agent ERPNext.
|
||||
|
||||
## Ce qui est généré (`out/`, non commité)
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---|---|
|
||||
| `apply_plan.json` | **Run-book ordonné** : 6 étapes (SPEC §7) — responsable (worker/vps), statut, commande worker, artefacts produits, dépendances, confirmations préalables, lien doc. |
|
||||
| `MANIFEST.json` | **Manifeste agrégé** : comptes consolidés des 3 volets, items « à confirmer VPS » fusionnés (DocType custom / Company / rôles `equipe`), recoupement de cohérence (bijectivité 50 rôles). |
|
||||
|
||||
## Run-book généré (SPEC §7 · ordre d'application VPS)
|
||||
|
||||
| # | Responsable | Étape | Dépend de |
|
||||
|---|---|---|---|
|
||||
| 1 | worker | Générer fixtures `Role` + `Custom DocPerm` | — |
|
||||
| 2 | vps | Créer les DocTypes DTP custom manquants (après confirmation) | 1 |
|
||||
| 3 | vps | Déposer `role.json` + `custom_docperm.json` dans `fixtures/` + `bench migrate` | 1, 2 |
|
||||
| 4 | worker+vps | Matérialiser les `User Permission` row-level par utilisateur | 3 |
|
||||
| 5 | worker+vps | Importer les `Role Profile` (**après** les `Role`) + affecter `User.role_profile_name` | 3 |
|
||||
| 6 | vps | Vérification HTTP post-déploiement + audit QA 4Big | 4, 5 |
|
||||
|
||||
## Utilisation
|
||||
|
||||
```bash
|
||||
# Génère out/apply_plan.json + out/MANIFEST.json
|
||||
python3 rbac_apply_plan.py build # [-o DOSSIER]
|
||||
|
||||
# Valide le plan (schéma + invariants) sans rien écrire
|
||||
python3 rbac_apply_plan.py validate
|
||||
|
||||
# Tests (stdlib pur, zéro pip)
|
||||
python3 -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
## Garde-fous (anti-invention · #6 · déterminisme)
|
||||
|
||||
- **Zéro chiffre recalculé** : chaque compte provient du manifeste du builder
|
||||
source (fixtures / userperm / roleprofile). Chaque item « à confirmer VPS » est
|
||||
repris tel quel. Le CLI **refuse d'écrire** si un invariant casse.
|
||||
- **Cohérence inter-volets** : les trois builders doivent dériver du **même
|
||||
contrat** (même `source_version`, même cible 50) — un mélange lève `ValueError`.
|
||||
- **Couverture bijective** re-vérifiée à travers les 3 volets : `Role` fixtures ==
|
||||
cible == entrées du plan `User Permission` == rôles couverts par `Role Profile`.
|
||||
- **Graphe de dépendances** validé : aucune dépendance en avant, aucun renvoi
|
||||
fantôme ; `roleprofile-apply` dépend bien de `fixtures-migrate` (Role avant
|
||||
Role Profile). Aucune confirmation orpheline (item non vide sans étape qui la
|
||||
cite) ni fantôme (étape citant une clé inexistante).
|
||||
- Sortie **déterministe** (ordre d'étapes fixe, listes triées, aucun horodatage)
|
||||
→ diffable, re-générable bit-à-bit en CI.
|
||||
|
||||
## Vérification en-repo
|
||||
|
||||
- `python3 -m unittest discover -s tests -v` → **16/16 verts** (schéma maison +
|
||||
oracle `jsonschema` si présent ; fidélité des comptes aux manifestes source,
|
||||
fusion des confirmations, ordre + graphe SPEC §7, garde-fous de cohérence,
|
||||
déterminisme).
|
||||
- Génération réelle : **6 étapes** · 50 rôles / 116 DocPerm / 28 UP templates /
|
||||
6 Role Profile · confirmations VPS 4 DocType custom + 5 Company + 4 rôles
|
||||
`equipe`.
|
||||
- Job CI dédié `rbac-applyplan-tests` ajouté au **gate** (`.gitea/workflows/ci.yml`,
|
||||
Gitea Actions uniquement · #2).
|
||||
|
||||
## Auto-score 4Big du livrable : **96/100**
|
||||
|
||||
_Réserve −4_ : l'exécution réelle du run-book (`bench migrate`, création des
|
||||
DocTypes custom, affectation `role_profile_name` / matérialisation des
|
||||
`User Permission` par utilisateur) reste côté VPS (agent ERPNext, hors périmètre
|
||||
worker, contrainte #8). Validé statiquement en-repo (16 tests verts + schéma
|
||||
conforme + recoupement bijectif des 3 volets + gate CI).
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://oto-enterprise-os-dtp/rbac/apply_plan.schema.json",
|
||||
"title": "Run-book d'application RBAC unifié (agrégat des 3 volets)",
|
||||
"description": "Contrat de sortie de rbac_apply_plan.py. Validé par le validateur maison Publiciste (zéro pip) dans le job CI rbac-applyplan-tests. Consolide les manifestes des 3 générateurs RBAC (fixtures Role+DocPerm, plan User Permission, Role Profile) en un plan d'application VPS ordonné (SPEC §7) + un manifeste agrégé recoupant leur cohérence. Aucun chiffre recalculé : tout compte provient d'un builder source.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["manifest", "apply_plan"],
|
||||
"properties": {
|
||||
"manifest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"generated_from",
|
||||
"source_version",
|
||||
"cible_rbac_roles",
|
||||
"counts",
|
||||
"confirmations_vps",
|
||||
"consistency"
|
||||
],
|
||||
"properties": {
|
||||
"generated_from": { "type": "string", "const": "rbac_50_roles.json" },
|
||||
"source_version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
|
||||
"cible_rbac_roles": { "type": "integer", "const": 50 },
|
||||
"counts": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"roles",
|
||||
"custom_docperm",
|
||||
"doctypes_uniques",
|
||||
"user_permission_templates",
|
||||
"role_profiles"
|
||||
],
|
||||
"properties": {
|
||||
"roles": { "type": "integer", "minimum": 50, "maximum": 50 },
|
||||
"custom_docperm": { "type": "integer", "minimum": 1 },
|
||||
"doctypes_uniques": { "type": "integer", "minimum": 1 },
|
||||
"user_permission_templates": { "type": "integer", "minimum": 0 },
|
||||
"role_profiles": { "type": "integer", "minimum": 1 }
|
||||
}
|
||||
},
|
||||
"confirmations_vps": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["custom_doctypes", "companies", "roles_scope_equipe"],
|
||||
"properties": {
|
||||
"custom_doctypes": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"companies": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"roles_scope_equipe": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"consistency": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"sources_coherentes",
|
||||
"source_version",
|
||||
"cible_rbac_roles",
|
||||
"roles_fixtures",
|
||||
"userperm_plan_entries",
|
||||
"roleprofile_roles_couverts",
|
||||
"couverture_bijective"
|
||||
],
|
||||
"properties": {
|
||||
"sources_coherentes": { "type": "boolean", "const": true },
|
||||
"source_version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
|
||||
"cible_rbac_roles": { "type": "integer", "const": 50 },
|
||||
"roles_fixtures": { "type": "integer", "minimum": 0 },
|
||||
"userperm_plan_entries": { "type": "integer", "minimum": 0 },
|
||||
"roleprofile_roles_couverts": { "type": "integer", "minimum": 0 },
|
||||
"couverture_bijective": { "type": "boolean", "const": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"apply_plan": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/definitions/step" }
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"step": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"order",
|
||||
"id",
|
||||
"titre",
|
||||
"responsable",
|
||||
"statut",
|
||||
"commande",
|
||||
"produces",
|
||||
"depends_on",
|
||||
"confirmations",
|
||||
"doc"
|
||||
],
|
||||
"properties": {
|
||||
"order": { "type": "integer", "minimum": 1 },
|
||||
"id": { "type": "string", "pattern": "^[a-z0-9-]+$" },
|
||||
"titre": { "type": "string", "minLength": 1 },
|
||||
"responsable": { "type": "string", "enum": ["worker", "vps", "worker+vps"] },
|
||||
"statut": { "type": "string", "minLength": 1 },
|
||||
"commande": { "type": ["string", "null"] },
|
||||
"produces": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"depends_on": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "pattern": "^[a-z0-9-]+$" }
|
||||
},
|
||||
"confirmations": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "enum": ["custom_doctypes", "companies", "roles_scope_equipe"] }
|
||||
},
|
||||
"doc": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Agrégateur RBAC : run-book d'application VPS unifié (Sprint 2).
|
||||
|
||||
Ce paquet ne dépend que des trois builders RBAC déjà livrés
|
||||
(`fixturelib`, `permlib`, `profilelib`) — il ne ré-implémente aucune logique
|
||||
métier (workflow #5 : zéro duplication). Il consolide leurs sorties en un
|
||||
**plan d'application ordonné** unique (SPEC §7) + un manifeste agrégé.
|
||||
"""
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Agrégation des trois volets RBAC en un plan d'application VPS unique.
|
||||
|
||||
Pourquoi cet agrégateur
|
||||
-----------------------
|
||||
Trois générateurs RBAC sont livrés en-repo, chacun répondant à UNE question :
|
||||
- `fixtures_gen` → « QUELS verbes sur QUEL DocType » (Role + Custom DocPerm).
|
||||
- `userperm_gen` → « SUR QUELLES LIGNES » (scope_donnees → User Permission).
|
||||
- `roleprofile_gen`→ « QUEL bundle assignable » (Role Profile par portail).
|
||||
Il manquait la **vue d'ensemble** : dans quel ORDRE l'agent ERPNext applique-t-il
|
||||
ces trois sorties sur le VPS, avec quelles dépendances et quelles confirmations
|
||||
préalables ? C'est le rôle de ce module : produire le **run-book** (SPEC §7)
|
||||
consolidé, plus un manifeste agrégé qui recoupe la cohérence des trois volets.
|
||||
|
||||
Ce module NE ré-implémente RIEN : il APPELLE les trois builders et lit leurs
|
||||
manifestes. Il n'invente aucun chiffre (#6) — tout compte vient d'un builder ;
|
||||
tout item « à confirmer VPS » est repris tel quel du manifeste d'origine.
|
||||
|
||||
Déterminisme : ordre des étapes fixe (SPEC §7), listes de confirmations triées,
|
||||
aucun horodatage → sortie diffable et re-générable bit-à-bit à contrat constant.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
# Étapes du run-book (SPEC §7). L'ordre et les dépendances sont du SAVOIR
|
||||
# procédural documenté (pas des chiffres inventés) : ils encodent les contraintes
|
||||
# d'application ERPNext v15 (ex. les `Has Role` d'un Role Profile référencent des
|
||||
# `Role` qui doivent exister → import Role AVANT Role Profile).
|
||||
# `responsable` : worker (livré en-repo) | vps (agent ERPNext) | worker+vps.
|
||||
_STEPS: list[dict[str, Any]] = [
|
||||
{
|
||||
"order": 1,
|
||||
"id": "fixtures-generate",
|
||||
"titre": "Générer les fixtures Role + Custom DocPerm",
|
||||
"responsable": "worker",
|
||||
"statut": "livré",
|
||||
"commande": "python3 fixtures_gen/rbac_fixtures_gen.py build",
|
||||
"produces": ["role.json", "custom_docperm.json"],
|
||||
"depends_on": [],
|
||||
"confirmations": [],
|
||||
"doc": "../fixtures_gen/README.md",
|
||||
},
|
||||
{
|
||||
"order": 2,
|
||||
"id": "custom-doctypes-create",
|
||||
"titre": "Créer les DocTypes DTP custom manquants (après confirmation)",
|
||||
"responsable": "vps",
|
||||
"statut": "à faire VPS",
|
||||
"commande": None,
|
||||
"produces": [],
|
||||
"depends_on": ["fixtures-generate"],
|
||||
"confirmations": ["custom_doctypes"],
|
||||
"doc": "../RBAC_50_ROLES_SPEC.md",
|
||||
},
|
||||
{
|
||||
"order": 3,
|
||||
"id": "fixtures-migrate",
|
||||
"titre": "Déposer role.json + custom_docperm.json dans fixtures/ puis bench migrate",
|
||||
"responsable": "vps",
|
||||
"statut": "à faire VPS",
|
||||
"commande": None,
|
||||
"produces": [],
|
||||
"depends_on": ["fixtures-generate", "custom-doctypes-create"],
|
||||
"confirmations": [],
|
||||
"doc": "../fixtures_gen/README.md",
|
||||
},
|
||||
{
|
||||
"order": 4,
|
||||
"id": "userperm-apply",
|
||||
"titre": "Matérialiser les User Permission row-level par utilisateur",
|
||||
"responsable": "worker+vps",
|
||||
"statut": "plan livré · matérialisation VPS",
|
||||
"commande": "python3 userperm_gen/userperm_gen.py build",
|
||||
"produces": ["user_permission_plan.json"],
|
||||
"depends_on": ["fixtures-migrate"],
|
||||
"confirmations": ["companies", "roles_scope_equipe"],
|
||||
"doc": "../userperm_gen/README.md",
|
||||
},
|
||||
{
|
||||
"order": 5,
|
||||
"id": "roleprofile-apply",
|
||||
"titre": "Importer les Role Profile (après les Role) puis affecter User.role_profile_name",
|
||||
"responsable": "worker+vps",
|
||||
"statut": "bundle livré · affectation VPS",
|
||||
"commande": "python3 roleprofile_gen/roleprofile_gen.py build",
|
||||
"produces": ["role_profile.json"],
|
||||
# Dépend de fixtures-migrate : les Has Role référencent des Role existants.
|
||||
"depends_on": ["fixtures-migrate"],
|
||||
"confirmations": [],
|
||||
"doc": "../roleprofile_gen/README.md",
|
||||
},
|
||||
{
|
||||
"order": 6,
|
||||
"id": "verify-http-qa",
|
||||
"titre": "Vérification HTTP post-déploiement + audit QA 4Big",
|
||||
"responsable": "vps",
|
||||
"statut": "à faire VPS",
|
||||
"commande": None,
|
||||
"produces": [],
|
||||
"depends_on": ["userperm-apply", "roleprofile-apply"],
|
||||
"confirmations": [],
|
||||
"doc": "../RBAC_50_ROLES_SPEC.md",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _require_same(label: str, values: list[Any]) -> Any:
|
||||
"""Renvoie l'unique valeur partagée par les trois volets, ou lève."""
|
||||
uniques = sorted({repr(v) for v in values})
|
||||
if len(uniques) != 1:
|
||||
raise ValueError(
|
||||
f"Volets RBAC incohérents sur {label} : {uniques} — "
|
||||
"les trois générateurs doivent dériver du MÊME contrat."
|
||||
)
|
||||
return values[0]
|
||||
|
||||
|
||||
def build_apply_plan(
|
||||
contract: dict,
|
||||
fixtures_bundle: dict,
|
||||
userperm_plan: dict,
|
||||
roleprofile_bundle: dict,
|
||||
) -> dict[str, Any]:
|
||||
"""Consolide les trois volets RBAC en run-book + manifeste agrégé.
|
||||
|
||||
Aucun chiffre n'est recalculé : chaque compte provient du manifeste du
|
||||
builder correspondant (source unique). La cohérence inter-volets (même
|
||||
version de contrat, même cible 50 rôles) est vérifiée et refuse un mélange.
|
||||
"""
|
||||
fm = fixtures_bundle["manifest"]
|
||||
um = userperm_plan["manifest"]
|
||||
pm = roleprofile_bundle["manifest"]
|
||||
|
||||
# Cohérence : les trois volets doivent dériver du même contrat (version + cible).
|
||||
source_version = _require_same(
|
||||
"source_version", [fm["source_version"], um["source_version"], pm["source_version"]]
|
||||
)
|
||||
cible = _require_same(
|
||||
"cible_rbac_roles",
|
||||
[fm["cible_rbac_roles"], um["cible_rbac_roles"], pm["cible_rbac_roles"]],
|
||||
)
|
||||
generated_from = _require_same(
|
||||
"generated_from", [fm["generated_from"], um["generated_from"], pm["generated_from"]]
|
||||
)
|
||||
|
||||
# Confirmations VPS consolidées (reprises telles quelles des manifestes source).
|
||||
confirmations_vps = {
|
||||
"custom_doctypes": sorted(fm.get("custom_doctypes_a_confirmer", [])),
|
||||
"companies": sorted(um.get("companies_a_confirmer", [])),
|
||||
"roles_scope_equipe": sorted(um.get("roles_scope_equipe_a_confirmer", [])),
|
||||
}
|
||||
|
||||
# Comptes agrégés : chaque valeur vient d'un manifeste de builder (pas d'invention).
|
||||
counts = {
|
||||
"roles": fm["counts"]["role"],
|
||||
"custom_docperm": fm["counts"]["custom_docperm"],
|
||||
"doctypes_uniques": fm["counts"]["doctypes_uniques"],
|
||||
"user_permission_templates": um["counts"]["user_permission_templates"],
|
||||
"role_profiles": pm["counts"]["role_profiles"],
|
||||
}
|
||||
|
||||
# Cohérence bijective : le nombre de rôles Role == cible == rôles couverts par
|
||||
# les Role Profile == entrées du plan User Permission (un plan par rôle).
|
||||
up_entries = um["counts"]["plan_entries"]
|
||||
rp_covered = pm["counts"]["roles_couverts"]
|
||||
consistency = {
|
||||
"sources_coherentes": True, # garanti par _require_same ci-dessus
|
||||
"source_version": source_version,
|
||||
"cible_rbac_roles": cible,
|
||||
"roles_fixtures": counts["roles"],
|
||||
"userperm_plan_entries": up_entries,
|
||||
"roleprofile_roles_couverts": rp_covered,
|
||||
"couverture_bijective": counts["roles"] == cible == up_entries == rp_covered,
|
||||
}
|
||||
|
||||
manifest = {
|
||||
"generated_from": generated_from,
|
||||
"source_version": source_version,
|
||||
"cible_rbac_roles": cible,
|
||||
"counts": counts,
|
||||
"confirmations_vps": confirmations_vps,
|
||||
"consistency": consistency,
|
||||
}
|
||||
|
||||
return {
|
||||
"manifest": manifest,
|
||||
"apply_plan": [dict(step) for step in _STEPS],
|
||||
}
|
||||
|
||||
|
||||
def step_ids() -> list[str]:
|
||||
"""IDs des étapes du run-book (pour les invariants du CLI/tests)."""
|
||||
return [s["id"] for s in _STEPS]
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Agrégateur RBAC · run-book d'application VPS unifié (Sprint 2).
|
||||
|
||||
Quatrième et dernier maillon RBAC en-repo — la **vue d'ensemble**. Les trois
|
||||
générateurs livrés répondent chacun à une question isolée :
|
||||
- `../fixtures_gen` → Role + Custom DocPerm (quels verbes / quel DocType).
|
||||
- `../userperm_gen` → plan User Permission row-level (sur quelles lignes).
|
||||
- `../roleprofile_gen`→ Role Profile par portail (quel bundle assignable).
|
||||
Il manquait l'**ordre d'application** consolidé : ce module produit le run-book
|
||||
(SPEC §7) + un manifeste agrégé qui recoupe la cohérence des trois volets (même
|
||||
contrat, même cible 50 rôles, couverture bijective).
|
||||
|
||||
Cible de portage VPS : agent ERPNext Backend. Ce worker n'écrit JAMAIS sur le
|
||||
VPS — il produit en-repo un plan ordonné que l'agent suit pas à pas.
|
||||
|
||||
Produit :
|
||||
- apply_plan.json → run-book ordonné (6 étapes SPEC §7 : responsable, statut,
|
||||
commande worker, artefacts produits, dépendances,
|
||||
confirmations préalables VPS, lien doc).
|
||||
- MANIFEST.json → manifeste agrégé (comptes consolidés des 3 volets, items
|
||||
« à confirmer VPS » fusionnés, recoupement de cohérence).
|
||||
|
||||
Sous-commandes :
|
||||
build [-o OUT] → écrit apply_plan.json / MANIFEST.json
|
||||
validate [-o OUT] → (re)génère en mémoire, valide vs apply_plan.schema.json
|
||||
+ invariants — sort en erreur si un invariant casse.
|
||||
|
||||
Sortie déterministe (ordre d'étapes fixe, listes triées, aucun horodatage).
|
||||
Anti-invention (#6) : aucun chiffre recalculé — tout compte provient d'un
|
||||
manifeste de builder ; chaque item « à confirmer » est repris tel quel.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_RBAC_DIR = os.path.normpath(os.path.join(_HERE, "..")) # rbac/
|
||||
_DELIVERABLES = os.path.normpath(os.path.join(_RBAC_DIR, "..")) # 05_deliverables_mvp/
|
||||
|
||||
# Réutilisation (workflow #5 : zéro duplication) des trois builders RBAC + du
|
||||
# validateur maison Publiciste. Paquets aux noms distincts (fixturelib / permlib
|
||||
# / profilelib / applylib / lib) → aucune collision de module.
|
||||
sys.path.insert(0, _HERE)
|
||||
sys.path.insert(0, os.path.join(_RBAC_DIR, "fixtures_gen"))
|
||||
sys.path.insert(0, os.path.join(_RBAC_DIR, "userperm_gen"))
|
||||
sys.path.insert(0, os.path.join(_RBAC_DIR, "roleprofile_gen"))
|
||||
sys.path.insert(0, os.path.join(_DELIVERABLES, "publiciste"))
|
||||
|
||||
from applylib import aggregator # noqa: E402
|
||||
from fixturelib import builder as fixtures_builder # type: ignore # noqa: E402
|
||||
from permlib import builder as userperm_builder # type: ignore # noqa: E402
|
||||
from profilelib import builder as roleprofile_builder # type: ignore # noqa: E402
|
||||
from lib import validator as maison # type: ignore # noqa: E402
|
||||
|
||||
_CONTRACT_PATH = os.path.join(_RBAC_DIR, "rbac_50_roles.json")
|
||||
_SCHEMA_PATH = os.path.join(_HERE, "apply_plan.schema.json")
|
||||
_DEFAULT_OUT = os.path.join(_HERE, "out")
|
||||
|
||||
|
||||
def _eprint(*args) -> None:
|
||||
print(*args, file=sys.stderr)
|
||||
|
||||
|
||||
def _load(path: str) -> dict:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def _write_json(path: str, data) -> None:
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump(data, fh, ensure_ascii=False, indent=2)
|
||||
fh.write("\n")
|
||||
|
||||
|
||||
def _build_plan() -> dict:
|
||||
"""Appelle les 3 builders sur le contrat, puis agrège en run-book."""
|
||||
contract = _load(_CONTRACT_PATH)
|
||||
return aggregator.build_apply_plan(
|
||||
contract,
|
||||
fixtures_builder.build_bundle(contract),
|
||||
userperm_builder.build_plan(contract),
|
||||
roleprofile_builder.build_bundle(contract),
|
||||
)
|
||||
|
||||
|
||||
def _validate_plan(plan: dict) -> list[str]:
|
||||
"""Valide le plan contre apply_plan.schema.json + invariants RBAC agrégés."""
|
||||
schema = _load(_SCHEMA_PATH)
|
||||
errors = list(maison.validate(plan, schema))
|
||||
|
||||
manifest = plan["manifest"]
|
||||
steps = plan["apply_plan"]
|
||||
|
||||
# Invariant 1 · le run-book couvre EXACTEMENT les étapes SPEC §7, dans l'ordre.
|
||||
expected_ids = aggregator.step_ids()
|
||||
got_ids = [s["id"] for s in steps]
|
||||
if got_ids != expected_ids:
|
||||
errors.append(f"apply_plan: étapes {got_ids} ≠ SPEC §7 {expected_ids}.")
|
||||
orders = [s["order"] for s in steps]
|
||||
if orders != sorted(orders) or orders != list(range(1, len(steps) + 1)):
|
||||
errors.append(f"apply_plan: champ `order` non séquentiel {orders}.")
|
||||
|
||||
# Invariant 2 · intégrité du graphe de dépendances (pas de renvoi fantôme,
|
||||
# pas de dépendance en avant → l'agent applique dans l'ordre sans blocage).
|
||||
id_to_order = {s["id"]: s["order"] for s in steps}
|
||||
for s in steps:
|
||||
for dep in s["depends_on"]:
|
||||
if dep not in id_to_order:
|
||||
errors.append(f"{s['id']}: dépendance inconnue {dep!r}.")
|
||||
elif id_to_order[dep] >= s["order"]:
|
||||
errors.append(f"{s['id']}: dépend d'une étape postérieure {dep!r}.")
|
||||
|
||||
# Invariant 3 · dépendance métier ERPNext critique : Role Profile importé
|
||||
# APRÈS la migration des fixtures Role (Has Role référence des Role existants).
|
||||
rp = next((s for s in steps if s["id"] == "roleprofile-apply"), None)
|
||||
if rp is not None and "fixtures-migrate" not in rp["depends_on"]:
|
||||
errors.append("roleprofile-apply doit dépendre de fixtures-migrate (Role avant Role Profile).")
|
||||
|
||||
# Invariant 4 · toute confirmation citée par une étape existe dans le manifeste
|
||||
# ET toute confirmation non vide du manifeste est référencée par au moins une
|
||||
# étape (aucun item « à confirmer » orphelin, aucune référence fantôme).
|
||||
conf = manifest["confirmations_vps"]
|
||||
referenced: set[str] = set()
|
||||
for s in steps:
|
||||
for key in s["confirmations"]:
|
||||
if key not in conf:
|
||||
errors.append(f"{s['id']}: confirmation inconnue {key!r}.")
|
||||
referenced.add(key)
|
||||
for key, items in conf.items():
|
||||
if items and key not in referenced:
|
||||
errors.append(f"confirmations_vps.{key} non vide mais référencée par aucune étape.")
|
||||
|
||||
# Invariant 5 · cohérence inter-volets : les 3 volets dérivent du même contrat.
|
||||
cons = manifest["consistency"]
|
||||
if not cons["sources_coherentes"]:
|
||||
errors.append("consistency.sources_coherentes=false (volets RBAC incohérents).")
|
||||
if cons["source_version"] != manifest["source_version"]:
|
||||
errors.append("consistency.source_version ≠ manifest.source_version.")
|
||||
if cons["cible_rbac_roles"] != manifest["cible_rbac_roles"]:
|
||||
errors.append("consistency.cible_rbac_roles ≠ manifest.cible_rbac_roles.")
|
||||
|
||||
# Invariant 6 · couverture bijective des 50 rôles à travers les 3 volets :
|
||||
# fixtures Role == cible == entrées plan UP == rôles couverts par Role Profile.
|
||||
cible = manifest["cible_rbac_roles"]
|
||||
if not (cons["roles_fixtures"] == cible == cons["userperm_plan_entries"] == cons["roleprofile_roles_couverts"]):
|
||||
errors.append(
|
||||
"Couverture non bijective : "
|
||||
f"Role={cons['roles_fixtures']}, cible={cible}, "
|
||||
f"UP={cons['userperm_plan_entries']}, RoleProfile={cons['roleprofile_roles_couverts']}."
|
||||
)
|
||||
if not cons["couverture_bijective"]:
|
||||
errors.append("consistency.couverture_bijective=false alors que les comptes concordent (ou l'inverse).")
|
||||
if manifest["counts"]["roles"] != cible:
|
||||
errors.append("counts.roles ≠ cible_rbac_roles (les fixtures ne couvrent pas les 50 rôles).")
|
||||
|
||||
# Invariant 7 · déterminisme : listes de confirmations triées.
|
||||
for key, items in conf.items():
|
||||
if items != sorted(items):
|
||||
errors.append(f"confirmations_vps.{key} non trié (déterminisme).")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def cmd_build(args: argparse.Namespace) -> int:
|
||||
plan = _build_plan()
|
||||
errors = _validate_plan(plan)
|
||||
if errors:
|
||||
_eprint("❌ Run-book invalide — génération refusée (anti-régression) :")
|
||||
for e in errors:
|
||||
_eprint(f" - {e}")
|
||||
return 1
|
||||
|
||||
out = os.path.abspath(args.out)
|
||||
os.makedirs(out, exist_ok=True)
|
||||
_write_json(os.path.join(out, "apply_plan.json"), plan["apply_plan"])
|
||||
_write_json(os.path.join(out, "MANIFEST.json"), plan["manifest"])
|
||||
|
||||
m = plan["manifest"]
|
||||
c = m["counts"]
|
||||
print(f"✅ Run-book RBAC généré dans {out}")
|
||||
print(
|
||||
f" apply_plan.json : {len(plan['apply_plan'])} étapes (SPEC §7) · "
|
||||
f"MANIFEST.json : {c['roles']} rôles, {c['custom_docperm']} DocPerm, "
|
||||
f"{c['user_permission_templates']} UP templates, {c['role_profiles']} Role Profile"
|
||||
)
|
||||
for s in plan["apply_plan"]:
|
||||
cmd = f" → {s['commande']}" if s["commande"] else ""
|
||||
print(f" {s['order']}. [{s['responsable']}] {s['titre']} ({s['statut']}){cmd}")
|
||||
conf = m["confirmations_vps"]
|
||||
print(
|
||||
f" Confirmations VPS : {len(conf['custom_doctypes'])} DocType custom, "
|
||||
f"{len(conf['companies'])} Company, {len(conf['roles_scope_equipe'])} rôles `equipe`."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_validate(args: argparse.Namespace) -> int:
|
||||
plan = _build_plan()
|
||||
errors = _validate_plan(plan)
|
||||
if errors:
|
||||
_eprint("❌ Validation KO :")
|
||||
for e in errors:
|
||||
_eprint(f" - {e}")
|
||||
return 1
|
||||
cons = plan["manifest"]["consistency"]
|
||||
print(
|
||||
f"✅ Validation OK — {len(plan['apply_plan'])} étapes, "
|
||||
f"couverture bijective {cons['roles_fixtures']}/50 sur les 3 volets, "
|
||||
f"schéma + invariants verts."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
p = argparse.ArgumentParser(
|
||||
description="Agrégateur RBAC : run-book d'application VPS unifié (3 volets → 1 plan ordonné)."
|
||||
)
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
pb = sub.add_parser("build", help="génère apply_plan.json / MANIFEST.json")
|
||||
pb.add_argument("-o", "--out", default=_DEFAULT_OUT, help="dossier de sortie (défaut: ./out)")
|
||||
pb.set_defaults(func=cmd_build)
|
||||
|
||||
pv = sub.add_parser("validate", help="valide le plan (schéma + invariants) sans écrire")
|
||||
pv.add_argument("-o", "--out", default=_DEFAULT_OUT, help="ignoré (compat)")
|
||||
pv.set_defaults(func=cmd_validate)
|
||||
|
||||
args = p.parse_args(argv)
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,182 @@
|
||||
"""Tests de l'agrégateur RBAC (run-book d'application VPS unifié).
|
||||
|
||||
Stdlib pur (`unittest`) → aucune install pip requise sur le runner Gitea. La
|
||||
bibliothèque `jsonschema`, si présente, sert d'oracle draft-07 en complément du
|
||||
validateur maison Publiciste (parité). Sinon elle est ignorée.
|
||||
|
||||
Couverture : schéma (maison + oracle), fidélité des comptes au manifeste de
|
||||
chaque builder source (anti-invention #6), fusion des confirmations VPS, ordre
|
||||
et graphe de dépendances du run-book (SPEC §7), garde-fou de cohérence
|
||||
inter-volets, déterminisme.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_MODULE_DIR = os.path.normpath(os.path.join(_HERE, "..")) # apply_plan/
|
||||
_RBAC_DIR = os.path.normpath(os.path.join(_MODULE_DIR, "..")) # rbac/
|
||||
_DELIVERABLES = os.path.normpath(os.path.join(_RBAC_DIR, "..")) # 05_deliverables_mvp/
|
||||
|
||||
for _p in (
|
||||
_MODULE_DIR,
|
||||
os.path.join(_RBAC_DIR, "fixtures_gen"),
|
||||
os.path.join(_RBAC_DIR, "userperm_gen"),
|
||||
os.path.join(_RBAC_DIR, "roleprofile_gen"),
|
||||
os.path.join(_DELIVERABLES, "publiciste"),
|
||||
):
|
||||
if _p not in sys.path:
|
||||
sys.path.insert(0, _p)
|
||||
|
||||
import rbac_apply_plan as cli # noqa: E402
|
||||
from applylib import aggregator # noqa: E402
|
||||
from fixturelib import builder as fixtures_builder # noqa: E402
|
||||
from permlib import builder as userperm_builder # noqa: E402
|
||||
from profilelib import builder as roleprofile_builder # noqa: E402
|
||||
from lib import validator as maison # noqa: E402
|
||||
|
||||
try:
|
||||
import jsonschema # type: ignore
|
||||
|
||||
_HAS_JSONSCHEMA = True
|
||||
except ImportError: # pragma: no cover
|
||||
_HAS_JSONSCHEMA = False
|
||||
|
||||
|
||||
def _load(path: str) -> dict:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
class ApplyPlanTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.contract = _load(cli._CONTRACT_PATH)
|
||||
cls.fixtures = fixtures_builder.build_bundle(cls.contract)
|
||||
cls.userperm = userperm_builder.build_plan(cls.contract)
|
||||
cls.roleprofile = roleprofile_builder.build_bundle(cls.contract)
|
||||
cls.plan = aggregator.build_apply_plan(
|
||||
cls.contract, cls.fixtures, cls.userperm, cls.roleprofile
|
||||
)
|
||||
cls.schema = _load(cli._SCHEMA_PATH)
|
||||
|
||||
# --- schéma ------------------------------------------------------------- #
|
||||
def test_schema_maison(self) -> None:
|
||||
errors = maison.validate(self.plan, self.schema)
|
||||
self.assertEqual(errors, [], f"validateur maison: {errors}")
|
||||
|
||||
@unittest.skipUnless(_HAS_JSONSCHEMA, "jsonschema absent (oracle optionnel)")
|
||||
def test_schema_oracle_jsonschema(self) -> None:
|
||||
jsonschema.validate(self.plan, self.schema)
|
||||
|
||||
def test_cli_validate_no_errors(self) -> None:
|
||||
self.assertEqual(cli._validate_plan(self.plan), [])
|
||||
|
||||
# --- fidélité des comptes (anti-invention #6) --------------------------- #
|
||||
def test_counts_come_from_source_manifests(self) -> None:
|
||||
c = self.plan["manifest"]["counts"]
|
||||
self.assertEqual(c["roles"], self.fixtures["manifest"]["counts"]["role"])
|
||||
self.assertEqual(c["custom_docperm"], self.fixtures["manifest"]["counts"]["custom_docperm"])
|
||||
self.assertEqual(c["doctypes_uniques"], self.fixtures["manifest"]["counts"]["doctypes_uniques"])
|
||||
self.assertEqual(
|
||||
c["user_permission_templates"],
|
||||
self.userperm["manifest"]["counts"]["user_permission_templates"],
|
||||
)
|
||||
self.assertEqual(c["role_profiles"], self.roleprofile["manifest"]["counts"]["role_profiles"])
|
||||
|
||||
def test_confirmations_merged_from_sources(self) -> None:
|
||||
conf = self.plan["manifest"]["confirmations_vps"]
|
||||
self.assertEqual(
|
||||
conf["custom_doctypes"],
|
||||
sorted(self.fixtures["manifest"]["custom_doctypes_a_confirmer"]),
|
||||
)
|
||||
self.assertEqual(
|
||||
conf["companies"], sorted(self.userperm["manifest"]["companies_a_confirmer"])
|
||||
)
|
||||
self.assertEqual(
|
||||
conf["roles_scope_equipe"],
|
||||
sorted(self.userperm["manifest"]["roles_scope_equipe_a_confirmer"]),
|
||||
)
|
||||
|
||||
# --- run-book (SPEC §7) ------------------------------------------------- #
|
||||
def test_steps_match_spec7_order(self) -> None:
|
||||
steps = self.plan["apply_plan"]
|
||||
self.assertEqual([s["id"] for s in steps], aggregator.step_ids())
|
||||
self.assertEqual([s["order"] for s in steps], list(range(1, len(steps) + 1)))
|
||||
|
||||
def test_dependency_graph_no_forward_edges(self) -> None:
|
||||
steps = self.plan["apply_plan"]
|
||||
order = {s["id"]: s["order"] for s in steps}
|
||||
for s in steps:
|
||||
for dep in s["depends_on"]:
|
||||
self.assertIn(dep, order)
|
||||
self.assertLess(order[dep], s["order"], f"{s['id']} dépend en avant de {dep}")
|
||||
|
||||
def test_roleprofile_after_role_fixtures(self) -> None:
|
||||
rp = next(s for s in self.plan["apply_plan"] if s["id"] == "roleprofile-apply")
|
||||
self.assertIn("fixtures-migrate", rp["depends_on"])
|
||||
|
||||
def test_every_confirmation_referenced(self) -> None:
|
||||
conf = self.plan["manifest"]["confirmations_vps"]
|
||||
referenced = {k for s in self.plan["apply_plan"] for k in s["confirmations"]}
|
||||
for key, items in conf.items():
|
||||
if items:
|
||||
self.assertIn(key, referenced, f"{key} non vide mais orpheline")
|
||||
|
||||
# --- cohérence inter-volets --------------------------------------------- #
|
||||
def test_consistency_bijective(self) -> None:
|
||||
cons = self.plan["manifest"]["consistency"]
|
||||
self.assertTrue(cons["sources_coherentes"])
|
||||
self.assertTrue(cons["couverture_bijective"])
|
||||
self.assertEqual(cons["roles_fixtures"], 50)
|
||||
self.assertEqual(cons["userperm_plan_entries"], 50)
|
||||
self.assertEqual(cons["roleprofile_roles_couverts"], 50)
|
||||
|
||||
def test_rejects_version_mismatch(self) -> None:
|
||||
"""Un volet dérivé d'un contrat de version différente → refus."""
|
||||
forged = json.loads(json.dumps(self.userperm))
|
||||
forged["manifest"]["source_version"] = "9.9.9"
|
||||
with self.assertRaises(ValueError):
|
||||
aggregator.build_apply_plan(self.contract, self.fixtures, forged, self.roleprofile)
|
||||
|
||||
def test_rejects_cible_mismatch(self) -> None:
|
||||
forged = json.loads(json.dumps(self.roleprofile))
|
||||
forged["manifest"]["cible_rbac_roles"] = 49
|
||||
with self.assertRaises(ValueError):
|
||||
aggregator.build_apply_plan(self.contract, self.fixtures, self.userperm, forged)
|
||||
|
||||
# --- déterminisme ------------------------------------------------------- #
|
||||
def test_deterministic(self) -> None:
|
||||
again = aggregator.build_apply_plan(
|
||||
self.contract, self.fixtures, self.userperm, self.roleprofile
|
||||
)
|
||||
self.assertEqual(
|
||||
json.dumps(self.plan, sort_keys=True, ensure_ascii=False),
|
||||
json.dumps(again, sort_keys=True, ensure_ascii=False),
|
||||
)
|
||||
|
||||
def test_confirmations_sorted(self) -> None:
|
||||
conf = self.plan["manifest"]["confirmations_vps"]
|
||||
for key, items in conf.items():
|
||||
self.assertEqual(items, sorted(items), f"{key} non trié")
|
||||
|
||||
# --- invariant-breaking detection --------------------------------------- #
|
||||
def test_validate_catches_broken_order(self) -> None:
|
||||
broken = json.loads(json.dumps(self.plan))
|
||||
broken["apply_plan"][0]["order"] = 99
|
||||
self.assertTrue(cli._validate_plan(broken))
|
||||
|
||||
def test_validate_catches_orphan_confirmation(self) -> None:
|
||||
broken = json.loads(json.dumps(self.plan))
|
||||
# Vide toutes les références de confirmation des étapes → items orphelins.
|
||||
for s in broken["apply_plan"]:
|
||||
s["confirmations"] = []
|
||||
self.assertTrue(cli._validate_plan(broken))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user