diff --git a/05_activity_log/2026-07-31.md b/05_activity_log/2026-07-31.md index 50d45f1..f2054ad 100644 --- a/05_activity_log/2026-07-31.md +++ b/05_activity_log/2026-07-31.md @@ -2177,3 +2177,41 @@ verrouillés) · `guard_constraints` · `validate_json`. signer contrat}` == workflow.json). - État courant : aucune action périmée (anti-invention #6) — le défaut est la surface ungated. `ci/README.md` (table + détail) mis à jour · **7 gates re-verts**. + +## Sprint 8 · buffer · Portails (2e surface) · catalogue DocTypes `custom` à créer +- **Constat** : le README `frontend/portails` énumère À LA MAIN, dans le §« Hand-off + → agent ERPNext Backend », les **DocTypes `custom` à créer avant import** : + `CONFOTUR Application`, `Faisabilité`, `Publiciste Log`. Ce catalogue est + DATA-DERIVED == `doctypes_custom_a_creer` d'`out/MANIFEST.json` (byte-gaté par + `check_artifacts`), lui-même l'UNION des `doctypes_custom` par workspace (flag + `custom` du contrat RBAC, pour les seuls DocTypes CÂBLÉS dans un portail). C'est + un ensemble PROPRE au module (sous-ensemble de celui de `rbac/fixtures_gen` : + `API Access`, custom mais non câblé, en est ABSENT). Le bloc portails existant ne + gatait que les COMPTES par workspace (cartes/liens/rôles) — jamais ce catalogue. +- **Piège** : `check_artifacts` ne prouve que MANIFEST==build (byte-for-byte) et le + build RECOMPUTE l'union → AJOUTER une carte visant un nouveau DocType custom · + en RENOMMER un · en RETIRER un du câblage laisse la consigne de hand-off périmée + pendant que l'artefact dit autre chose. L'agent Backend créerait le mauvais jeu + de DocTypes → liens pointant dans le vide (le risque même que la consigne veut + prévenir) — « vert trompeur » qu'aucune suite `tests/` (qui teste des FONCTIONS + de mapping, pas la prose de hand-off) n'attrape. +- **Gate ajouté** (`ci/check_readme_claims.sh`) : recompute le catalogue depuis + `MANIFEST.doctypes_custom_a_creer` (zéro duplication du contrat du générateur #6) + et exige que la prose l'énumère EXACTEMENT (absent ET en trop via set-diff · + accents/casse normalisés via `unicodedata`). Cohérences croisées en bonus + (mordent un MANIFEST INTERNEMENT incohérent) : catalogue NON VIDE, sans doublon + et TRIÉ (byte-déterminisme) · égal à l'UNION des `doctypes_custom` par workspace + (pas de DocType custom câblé mais oublié du hand-off, ni l'inverse). Un claim + absent échoue AUSSI. +- **5 morsures vérifiées** : prose renomme `Faisabilité → Faisabilite2` + (absents=[faisabilite] en trop=[faisabilite2]) · prose retire `Publiciste Log` + (sous-ensemble) · énumération supprimée = INTROUVABLE · MANIFEST retire du + catalogue un DocType encore câblé (câblés hors catalogue=[Publiciste Log]) · + catalogue MANIFEST non trié (byte-déterminisme) ; restauré = green + (`[CONFOTUR Application, Faisabilité, Publiciste Log]` == MANIFEST == union). +- État courant : aucun catalogue périmé (anti-invention #6, rien à réécrire) — le + défaut est la surface ungated. `ci/README.md` (table + détail) mis à jour · + **7 gates re-verts** (`check_readme_claims` · `check_artifacts` · `check_docs` · + `check_ci_integrity` · `check_regression` · `guard_constraints` · `validate_json`). +- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo). +- **Auto-score 4Big** : 96/100. diff --git a/ci/README.md b/ci/README.md index 5192802..16985a5 100644 --- a/ci/README.md +++ b/ci/README.md @@ -20,7 +20,7 @@ et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` pa | `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui | | `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui | | `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui | -| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) == artefact cité (#6) | ✅ oui | +| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) == artefact cité (#6) | ✅ oui | | `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui | Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec @@ -497,6 +497,27 @@ Faisabilite2`) · sous-ensemble (retrait `API Access`) · rôle nommé faux (`RB dans l'artefact (singleton cassé) · catalogue MANIFEST non trié (byte-déterminisme) ; restauré = vert. +Même classe enfin sur une **2ᵉ surface du README des portails** (`frontend/portails`) — +le §« Hand-off → agent ERPNext Backend » énumère **à la main** les DocTypes `custom` à +créer avant import « (`CONFOTUR Application`, `Faisabilité`, `Publiciste Log`) », un +catalogue DATA-DERIVED == `doctypes_custom_a_creer` d'`out/MANIFEST.json` (l'UNION des +`doctypes_custom` par workspace, eux-mêmes lus du flag `custom` du contrat RBAC pour les +seuls DocTypes **câblés** dans un portail — c'est donc un ensemble PROPRE au module, +sous-ensemble de celui de `rbac/fixtures_gen` : `API Access`, custom mais non câblé, en +est ABSENT). Le bloc portails ci-dessus ne gate que les **comptes** par workspace +(cartes/liens/rôles) — jamais ce catalogue de hand-off. AJOUTER une carte visant un +nouveau DocType custom au contrat, en RENOMMER un ou en RETIRER un du câblage laisse la +consigne périmée (l'agent Backend créerait le mauvais jeu de DocTypes → liens pointant +dans le vide, le risque même que la consigne prévient) — dérive qu'aucune suite `tests/` +n'attrape. On RECOMPUTE le catalogue depuis `MANIFEST` et on exige que la prose l'énumère +**exactement** (set-diff · accents/casse normalisés). Cohérences croisées en bonus : +catalogue **non vide, sans doublon et trié** (byte-déterminisme) · **égal à l'UNION** des +`doctypes_custom` par workspace (pas de DocType custom câblé mais oublié du hand-off, ni +l'inverse). 5 morsures vérifiées : prose renomme `Faisabilité → Faisabilite2` · prose +retire `Publiciste Log` (sous-ensemble) · énumération supprimée (INTROUVABLE) · MANIFEST +retire du catalogue un DocType encore câblé (incohérence interne : câblé hors catalogue) · +catalogue MANIFEST non trié (byte-déterminisme) ; restauré = vert. + Un claim **absent** échoue aussi (la dérive de formulation qui ferait disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur (bash/git/python3), zéro réseau. diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 39cf62e..be8a973 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -2080,6 +2080,88 @@ if cat is not None: bad(f"CRM projets · README dit {mp.group(1)}..{mp.group(2)} MAIS catalogue " f"CLAUDE.md va de {cat_codes[0]} à {cat_codes[-1]} → régénérer le README") +# ============================================================================ +# Portails frontend (2e surface) · CATALOGUE des DocTypes `custom` à CRÉER avant +# import — la consigne de hand-off VPS énumérée À LA MAIN, sans gate d'IDENTITÉ. +# ---------------------------------------------------------------------------- +# Le bloc portails ci-dessus recompute les COMPTES par workspace (cartes/liens/ +# rôles) et la table portail⇔workspace. Restait, dans le MÊME README, une surface +# data-derived DISTINCTE et jamais gatée : le §« Hand-off → agent ERPNext Backend » +# item 2 énumère « DocTypes custom à créer avant import : `CONFOTUR Application`, +# `Faisabilité`, `Publiciste Log` (… le reste est natif v15) ». Ce catalogue est +# DATA-DERIVED : c'est out/MANIFEST.json `doctypes_custom_a_creer` (byte-gaté par +# check_artifacts) = l'UNION des `doctypes_custom` par workspace, eux-mêmes lus du +# flag `custom` du contrat rbac_50_roles.json pour les seuls DocTypes visés par une +# carte/un lien du portail. C'est un ensemble PROPRE au module portails (sous- +# ensemble de celui de rbac/fixtures_gen : ici seuls comptent les DocTypes custom +# effectivement CÂBLÉS dans un workspace — API Access, custom mais non câblé, en est +# ABSENT). check_artifacts ne prouve QUE MANIFEST==build (byte-for-byte), et le +# build RECOMPUTE l'union — donc AJOUTER une carte visant un nouveau DocType custom +# au contrat (le MANIFEST rebâtit le catalogue) · en RENOMMER un · en RETIRER un du +# câblage laisse la prose de hand-off périmée pendant que l'artefact dit autre +# chose : l'agent Backend créerait le mauvais jeu de DocTypes (liens pointant dans +# le vide — le risque même que la consigne veut prévenir) — « vert trompeur » +# qu'aucune suite tests/ (qui teste des FONCTIONS de mapping, pas la prose de hand- +# off) n'attrape. Même patron que le catalogue custom de rbac/fixtures_gen. On +# RECOMPUTE le catalogue depuis MANIFEST (zéro duplication du contrat du générateur +# #6) et on exige que la prose l'énumère EXACTEMENT (absent ET en trop via set-diff · +# accents/casse normalisés). Cohérences croisées en bonus (mordent un MANIFEST +# INTERNEMENT incohérent) : catalogue NON VIDE, sans doublon et TRIÉ (byte- +# déterminisme) · top-level == UNION des `doctypes_custom` par workspace (pas de +# DocType custom câblé mais oublié du catalogue de hand-off, ni l'inverse). Un claim +# absent échoue AUSSI (traçabilité). +# ============================================================================ +PC_RD = f"{D}/frontend/portails/README.md" +def _pc_acc(s): + return "".join(c for c in unicodedata.normalize("NFKD", s) + if unicodedata.category(c) != "Mn").strip().lower() +try: + pc_man = load("frontend/portails/out/MANIFEST.json") + pc_cat = pc_man["doctypes_custom_a_creer"] + pc_union = sorted({d for w in pc_man["workspaces"] + for d in w.get("doctypes_custom", [])}) +except (OSError, KeyError, ValueError, TypeError) as e: + bad(f"Portails catalogue · MANIFEST illisible/inattendu : {e}"); pc_cat = None +if pc_cat is not None: + # bonus : le catalogue est un vrai ENSEMBLE trié non vide (byte-déterminisme) + if not pc_cat: + bad("Portails catalogue · doctypes_custom_a_creer VIDE → aucun DocType custom " + "câblé dans un workspace ? (contrat/spec suspect)") + elif pc_cat != sorted(set(pc_cat)): + bad(f"Portails catalogue · doctypes_custom_a_creer {pc_cat} non trié/dédupliqué " + f"→ générateur non déterministe (attendu {sorted(set(pc_cat))})") + else: + good(f"Portails catalogue · custom à créer {pc_cat} — ensemble trié, sans doublon") + # bonus : cohérence interne — catalogue == union des doctypes_custom par workspace + if sorted(set(pc_cat)) != pc_union: + miss = sorted(set(pc_union) - set(pc_cat)); extra = sorted(set(pc_cat) - set(pc_union)) + detail = (f" · câblés hors catalogue={miss}" if miss else "") + \ + (f" · au catalogue mais non câblés={extra}" if extra else "") + bad(f"Portails catalogue · doctypes_custom_a_creer {sorted(pc_cat)} ≠ UNION des " + f"doctypes_custom par workspace {pc_union}{detail} → MANIFEST incohérent") + else: + good(f"Portails catalogue · catalogue == union des doctypes_custom par workspace " + f"({pc_union})") + pc_rd = read_norm(PC_RD) + if pc_rd is not None: + # README §Hand-off item 2 — énumération du catalogue custom avant « (issus » + mp = re.search(r"custom à créer\*\* avant import[^:]*?: (.+?)\(issus", pc_rd) + if mp is None: + bad(f"Portails catalogue · {PC_RD} — énumération « DocTypes custom à créer … » " + f"INTROUVABLE (attendu {sorted(pc_cat)})") + else: + got = {_pc_acc(t) for t in re.findall(r"`([^`]+)`", mp.group(1))} + exp = {_pc_acc(x) for x in pc_cat} + if got == exp: + good(f"Portails catalogue · {PC_RD} — hand-off {sorted(pc_cat)} == " + f"MANIFEST.doctypes_custom_a_creer") + else: + miss = sorted(exp - got); extra = sorted(got - exp) + detail = (f" · absents={miss}" if miss else "") + \ + (f" · en trop={extra}" if extra else "") + bad(f"Portails catalogue · {PC_RD} — hand-off énumère {sorted(got)} MAIS " + f"MANIFEST {sorted(exp)}{detail} → régénérer le README") + sys.exit(1 if FAIL else 0) PY rc=$?