[DTP-Worker 20260803_030641] Auto exec · session 20260803_030641
CI / Contraintes NON-NÉGOCIABLES (CLAUDE.md) (push) Has been cancelled
CI / Validation JSON (schémas Faisabilité) (push) Has been cancelled
CI / Qualité documentaire (liens + 4Big) (push) Has been cancelled
CI / Reproductibilité des artefacts out/ (build == commité) (push) Has been cancelled
CI / Fraîcheur matrice de régression (run == commité) (push) Has been cancelled
CI / Intégrité du câblage CI (gate agrège tout · gates statiques verrouillés) (push) Has been cancelled
CI / Intégrité des chiffres du README (valeur == artefact cité · (push) Has been cancelled
CI / Publiciste · parser + schéma + generator (unittest) (push) Has been cancelled
CI / RBAC · 50 rôles + schéma (unittest) (push) Has been cancelled
CI / Faisabilité · générateur 4 volets + round-trip (unittest) (push) Has been cancelled
CI / RBAC · fixtures ERPNext (Role + Custom DocPerm) (push) Has been cancelled
CI / RBAC · plan User Permission (row-level) (push) Has been cancelled
CI / RBAC · Role Profile (bundles par portail) (push) Has been cancelled
CI / RBAC · run-book d'application unifié (agrégat 3 volets) (push) Has been cancelled
CI / Faisabilité · dossier bancable trilingue FR/EN/ES (push) Has been cancelled
CI / CRM · workflow vente ERPNext (lead → CONFOTUR) (push) Has been cancelled
CI / CRM · DocType porteur OTO Dossier Vente (push) Has been cancelled
CI / CRM · barème commissions vendeurs (push) Has been cancelled
CI / Fiscal · e-CF DGII (Compupar) (push) Has been cancelled
CI / Frontend · Workspaces 5 portails rôle (push) Has been cancelled
CI / Legal · DocType CONFOTUR Application (push) Has been cancelled
CI / QA · Audit 5D conformité (push) Has been cancelled
CI / SEO · mots-clés trilingues + schema.org + hreflang (push) Has been cancelled
CI / Chat OTOIA · montage par portail (Custom Block) (push) Has been cancelled
CI / QA · Audit 4Big (95+/100 sur 100% deliverables) (push) Has been cancelled
CI / Démo · Scénarios (run-sheet P07 banquier / P05 client) (push) Has been cancelled
CI / QA · Matrice de régression exhaustive (Sprint 8) (push) Has been cancelled
CI / DevOps · Run-book de déploiement VPS unifié (Sprint 8) (push) Has been cancelled
CI / QA · Matrice d'acceptation / traçabilité MVP (Sprint 8) (push) Has been cancelled
CI / Mobile · config app Expo/EAS (navigation par rôle) (push) Has been cancelled
CI / E2E baseline Playwright (manuel) (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled

This commit is contained in:
Claude Code DTP Worker
2026-08-03 03:21:42 +00:00
parent 3eec69a87f
commit d0b354f81e
14 changed files with 302 additions and 21 deletions
@@ -33,6 +33,7 @@ Ce livrable ne fabrique **aucun fait** :
| Tokens de marque `#0a0a12`/`#f0b429` · thème dark | `CLAUDE.md #4` (via `branding.py`) |
| Devises USD/DOP | `CLAUDE.md #10` (via `branding.py`) |
| Expo SDK **54** | roadmap Sprint 5 l.56 |
| Fiche store : nb + énumération portails, langues | **injectés** depuis `role_navigation` (labels portails_spec) + seo (jamais recopiés) — seuls le style rédactionnel + mots-clés sont authoriaux (comme `app.name`) |
| **bundleIdentifier · package · projectId EAS · version · credentials · appleId · ascAppId** | **`null` · `a_confirmer`** — jamais fabriqués (#6/#8) |
**Ancrage de sécurité** : un onglet n'est visible que pour les **rôles RBAC exacts**
@@ -45,7 +46,7 @@ Publiciste (zéro `pip`).
```bash
python3 app_config_gen.py build # écrit out/{app_config,eas_build,role_navigation}.json + MANIFEST.json
python3 app_config_gen.py validate # schéma + 12 invariants (sort en erreur sinon)
python3 app_config_gen.py validate # schéma + 15 invariants (sort en erreur sinon)
python3 -m unittest discover -s tests -v
```
@@ -56,6 +57,10 @@ python3 -m unittest discover -s tests -v
- `eas_build.json` — objet `eas.json` (profils `development`/`preview`/`production`
+ `submit.production` avec credentials `null`).
- `role_navigation.json` — 1 onglet par portail, `roles_allowed` = surface RBAC exacte.
- `store_listing.json` — fiche store FR/EN/ES (sous-titre, description courte/longue,
mots-clés) **dérivée** : nombre + énumération des portails et langues **injectés**
depuis `role_navigation`/seo (jamais recopiés · #6) ; limites de caractères App
Store / Play validées au build.
- `MANIFEST.json` — traçabilité (comptes, résumé par portail, marque, `a_confirmer`).
Sortie **déterministe** (tri stable, aucun horodatage) → diffable + re-générable ;
@@ -64,8 +69,8 @@ validée par le **validateur maison Publiciste** contre `mobile.schema.json`.
## Résultat courant
**5 onglets** (Ventes/Construction/Achat/Compta/Direction) · **44 rôles** couverts ·
**3 langues** · **13 identifiants `a_confirmer`** (jamais fabriqués). Schéma + 12
invariants verts.
**3 langues** · **13 identifiants `a_confirmer`** (jamais fabriqués) · fiche store
FR/EN/ES dérivée. Schéma + 15 invariants verts.
## Hors périmètre worker (VPS · #8)
@@ -12,6 +12,8 @@ en :
identifiants de store `null` · a_confirmer)
- eas_build.json -> objet `eas.json` (profils build/submit, credentials `null`)
- role_navigation.json -> 1 onglet par portail, gated par la surface RBAC exacte
- store_listing.json -> fiche store FR/EN/ES (sous-titre, descriptions, mots-clés)
dérivée : portails/langues injectés depuis les sources (#6)
- MANIFEST.json -> traçabilité (comptes, résumé par portail, marque, a_confirmer)
Sous-commandes :
@@ -179,7 +181,44 @@ def _validate_bundle(bundle: dict) -> list[str]:
if c.get(k) != v:
errors.append(f"counts.{k} incohérent ({c.get(k)} != {v}).")
# Invariant 12 · déterminisme : deux builds successifs identiques.
# Invariant 12 · fiche store : couvre EXACTEMENT les langues seo (aucune inventée).
sl = bundle["store_listing"]
sl_spec = spec["store_listing"]
if sorted(sl["content"].keys()) != sorted(langs):
errors.append("store_listing.content != langues seo (FR/EN/ES).")
if sl["langues"] != list(langs):
errors.append("store_listing.langues != langues seo.")
if sl["limits"] != sl_spec["limits"]:
errors.append("store_listing.limits != spec.store_listing.limits.")
# Invariant 13 · chaque champ de fiche respecte sa limite de caractères store
# (App Store / Play — voir limits_source) et n'est jamais vide.
for lang, fields in sl["content"].items():
for champ, limit in sl_spec["limits"].items():
val = fields.get(champ)
if not val:
errors.append(f"store_listing[{lang}].{champ} vide/absent.")
elif len(val) > limit:
errors.append(
f"store_listing[{lang}].{champ} dépasse la limite store {limit} "
f"caractères (={len(val)})."
)
# Invariant 14 · ANTI-DÉRIVE : l'énumération des portails et les langues injectées
# dans chaque description longue == la surface RÉELLE (labels role_navigation ·
# langues seo) — la copie marketing ne peut pas mentir sur les portails/langues.
portails_str = " · ".join(n["label"] for n in nav)
langues_str = "/".join(lang.upper() for lang in langs)
for lang, fields in sl["content"].items():
full = fields.get("full_description", "")
if portails_str not in full:
errors.append(f"store_listing[{lang}].full_description n'injecte pas les portails réels.")
if langues_str not in full:
errors.append(f"store_listing[{lang}].full_description n'injecte pas les langues réelles.")
if "{" in full or "}" in full:
errors.append(f"store_listing[{lang}].full_description : placeholder non substitué.")
# Invariant 15 · déterminisme : deux builds successifs identiques.
again = _build_bundle()
if json.dumps(again, sort_keys=True, ensure_ascii=False) != json.dumps(bundle, sort_keys=True, ensure_ascii=False):
errors.append("build non déterministe (deux passes divergent).")
@@ -201,6 +240,7 @@ def cmd_build(args: argparse.Namespace) -> int:
_write_json(os.path.join(out, "app_config.json"), bundle["app_config"])
_write_json(os.path.join(out, "eas_build.json"), bundle["eas_build"])
_write_json(os.path.join(out, "role_navigation.json"), bundle["role_navigation"])
_write_json(os.path.join(out, "store_listing.json"), bundle["store_listing"])
_write_json(os.path.join(out, "MANIFEST.json"), bundle["manifest"])
c = bundle["manifest"]["counts"]
@@ -225,7 +265,7 @@ def cmd_validate(args: argparse.Namespace) -> int:
c = bundle["manifest"]["counts"]
print(
f"✅ Validation OK — {c['onglets']} onglets, {c['roles_couverts']} rôles, "
f"schéma + 12 invariants verts (identifiants a_confirmer)."
f"schéma + 15 invariants verts (identifiants a_confirmer · fiche store dérivée)."
)
return 0
@@ -3,7 +3,20 @@
"title": "Config app mobile OTO Enterprise OS (Expo/EAS + navigation par rôle)",
"type": "object",
"additionalProperties": false,
"required": ["app_config", "eas_build", "role_navigation", "manifest"],
"required": ["app_config", "eas_build", "role_navigation", "store_listing", "manifest"],
"definitions": {
"store_listing_lang": {
"type": "object",
"additionalProperties": false,
"required": ["subtitle", "short_description", "full_description", "keywords"],
"properties": {
"subtitle": {"type": "string", "minLength": 1},
"short_description": {"type": "string", "minLength": 1},
"full_description": {"type": "string", "minLength": 1},
"keywords": {"type": "string", "minLength": 1}
}
}
},
"properties": {
"app_config": {
"type": "object",
@@ -155,6 +168,38 @@
}
}
},
"store_listing": {
"type": "object",
"additionalProperties": false,
"required": ["generated_from", "note", "limits", "limits_source", "langues", "content"],
"properties": {
"generated_from": {"type": "string", "minLength": 1},
"note": {"type": "string", "minLength": 1},
"limits": {
"type": "object",
"additionalProperties": false,
"required": ["subtitle", "short_description", "full_description", "keywords"],
"properties": {
"subtitle": {"type": "integer", "minimum": 1},
"short_description": {"type": "integer", "minimum": 1},
"full_description": {"type": "integer", "minimum": 1},
"keywords": {"type": "integer", "minimum": 1}
}
},
"limits_source": {"type": "string", "minLength": 1},
"langues": {"type": "array", "minItems": 1, "items": {"type": "string"}},
"content": {
"type": "object",
"additionalProperties": false,
"required": ["fr", "en", "es"],
"properties": {
"fr": {"$ref": "#/definitions/store_listing_lang"},
"en": {"$ref": "#/definitions/store_listing_lang"},
"es": {"$ref": "#/definitions/store_listing_lang"}
}
}
}
},
"manifest": {
"type": "object",
"additionalProperties": false,
@@ -33,6 +33,35 @@
{"champ": "eas_build.submit.production.ios.appleTeamId", "raison": "identifiant équipe Apple — hors repo (#6/#8)"},
{"champ": "eas_build.submit.production.android.serviceAccountKeyPath", "raison": "clé de service Google Play — secret hors repo (#6/#8)"}
],
"store_listing": {
"limits": {"subtitle": 30, "short_description": 80, "full_description": 4000, "keywords": 100},
"limits_source": "App Store Connect (sous-titre 30 · mots-clés 100 · description 4000 caractères) · Google Play Console (description courte 80 · complète 4000 caractères)",
"placeholders": {
"n_portails": "nombre d'onglets = portails métier (len role_navigation)",
"portails": "labels des portails joints par ' · ' (source portails_spec via role_navigation) — jamais recopiés à la main",
"langues": "langues seo en capitales jointes par '/' (source seo_spec)"
},
"content": {
"fr": {
"subtitle": "Vos portails métier en poche",
"short_description": "L'app compagnon de votre ERP OTO : {n_portails} portails métier, sécurisés par rôle.",
"full_description": "OTO Enterprise OS est l'application mobile compagnon de la plateforme immobilière OTO.\n\nRetrouvez vos {n_portails} portails métier — {portails} — directement sur votre téléphone.\n\nSécurité par rôle (RBAC) : chaque portail n'expose que les fonctions autorisées à votre profil ; aucune donnée hors de votre périmètre.\n\nInterface luxury sombre et dorée, cohérente avec la console web. Disponible en {langues}. Devises USD et DOP.\n\nL'app ne réplique aucune logique métier : elle est la surface mobile sécurisée de votre ERP OTO Enterprise OS.",
"keywords": "immobilier,ERP,CRM,ventes,construction,chantier,compta,portail,RBAC,OTO"
},
"en": {
"subtitle": "Business portals on the go",
"short_description": "The companion app for your OTO ERP: {n_portails} role-secured business portals.",
"full_description": "OTO Enterprise OS is the mobile companion app for the OTO real-estate platform.\n\nAccess your {n_portails} business portals — {portails} — right from your phone.\n\nRole-based security (RBAC): each portal only exposes the features your profile is allowed to use; no data outside your scope.\n\nLuxury dark-and-gold interface, consistent with the web console. Available in {langues}. USD and DOP currencies.\n\nThe app replicates no business logic: it is the secure mobile surface of your OTO Enterprise OS ERP.",
"keywords": "real estate,ERP,CRM,sales,construction,site,accounting,portal,RBAC,OTO,property"
},
"es": {
"subtitle": "Tus portales en movilidad",
"short_description": "La app complementaria de tu ERP OTO: {n_portails} portales seguros por rol.",
"full_description": "OTO Enterprise OS es la aplicación móvil complementaria de la plataforma inmobiliaria OTO.\n\nAccede a tus {n_portails} portales de negocio — {portails} — desde tu teléfono.\n\nSeguridad por rol (RBAC): cada portal solo muestra las funciones que tu perfil tiene permitidas; ningún dato fuera de tu alcance.\n\nInterfaz luxury oscura y dorada, coherente con la consola web. Disponible en {langues}. Monedas USD y DOP.\n\nLa app no replica ninguna lógica de negocio: es la superficie móvil segura de tu ERP OTO Enterprise OS.",
"keywords": "inmobiliaria,ERP,CRM,ventas,construcción,obra,contabilidad,portal,RBAC,OTO"
}
}
},
"notes": [
"L'app mobile ne réplique AUCUNE logique métier : elle est la surface mobile des 5 portails rôle ERPNext (mêmes rôles que les Workspaces / le Chat OTOIA).",
"Sécurité : un onglet n'est visible que pour les rôles RBAC du portail correspondant (roles_allowed = surface RBAC exacte, ancrée sur le contrat).",
@@ -117,10 +117,52 @@ def build_navigation(contract: dict, spec: dict, portails_spec: dict) -> list[di
return nav
def build_store_listing(spec: dict, nav: list[dict], langs: list[str]) -> dict[str, Any]:
"""Fiche store (App Store / Play) DÉRIVÉE des sources · #6.
Le worker ne SOUMET pas aux stores (agent Mobile · #8) : il produit la fiche
versionnable (sous-titre, descriptions, mots-clés FR/EN/ES). Les faits produit
prêts à dériver — nombre de portails, énumération des portails, langues — sont
INJECTÉS depuis la surface réelle (`role_navigation` = labels portails_spec,
langues seo), jamais recopiés à la main : la copie marketing ne peut donc pas
mentir sur les portails/langues. Seuls le style rédactionnel et les mots-clés
sont des choix authoriaux (mêmes que `app.name`), portés par la spec.
"""
sl_spec = spec["store_listing"]
portails_str = " · ".join(n["label"] for n in nav)
langues_str = "/".join(lang.upper() for lang in langs)
n_portails = str(len(nav))
def _render(text: str) -> str:
return (
text.replace("{n_portails}", n_portails)
.replace("{portails}", portails_str)
.replace("{langues}", langues_str)
)
content = {
lang: {champ: _render(text) for champ, text in sl_spec["content"][lang].items()}
for lang in langs
}
return {
"generated_from": "mobile_spec.json · store_listing (gabarits rédactionnels) + role_navigation (portails) + seo_spec (langues)",
"note": (
"Fiche store DÉRIVÉE : nombre/énumération des portails et langues injectés "
"depuis les sources (jamais recopiés · #6). Le worker ne build ni ne submit "
"aux stores — hand-off agent Mobile (#8)."
),
"limits": sl_spec["limits"],
"limits_source": sl_spec["limits_source"],
"langues": list(langs),
"content": content,
}
def build_bundle(contract: dict, spec: dict, portails_spec: dict, langs: list[str], default_lang: str) -> dict[str, Any]:
app_config = build_app_config(spec, langs, default_lang)
eas_build = build_eas(spec)
nav = build_navigation(contract, spec, portails_spec)
store_listing = build_store_listing(spec, nav, langs)
b = deps.branding
portails_meta = [
@@ -180,5 +222,6 @@ def build_bundle(contract: dict, spec: dict, portails_spec: dict, langs: list[st
"app_config": app_config,
"eas_build": eas_build,
"role_navigation": nav,
"store_listing": store_listing,
"manifest": manifest,
}
@@ -0,0 +1,36 @@
{
"generated_from": "mobile_spec.json · store_listing (gabarits rédactionnels) + role_navigation (portails) + seo_spec (langues)",
"note": "Fiche store DÉRIVÉE : nombre/énumération des portails et langues injectés depuis les sources (jamais recopiés · #6). Le worker ne build ni ne submit aux stores — hand-off agent Mobile (#8).",
"limits": {
"subtitle": 30,
"short_description": 80,
"full_description": 4000,
"keywords": 100
},
"limits_source": "App Store Connect (sous-titre 30 · mots-clés 100 · description 4000 caractères) · Google Play Console (description courte 80 · complète 4000 caractères)",
"langues": [
"fr",
"en",
"es"
],
"content": {
"fr": {
"subtitle": "Vos portails métier en poche",
"short_description": "L'app compagnon de votre ERP OTO : 5 portails métier, sécurisés par rôle.",
"full_description": "OTO Enterprise OS est l'application mobile compagnon de la plateforme immobilière OTO.\n\nRetrouvez vos 5 portails métier — OTO Ventes · OTO Construction · OTO Achat · OTO Compta · OTO Direction — directement sur votre téléphone.\n\nSécurité par rôle (RBAC) : chaque portail n'expose que les fonctions autorisées à votre profil ; aucune donnée hors de votre périmètre.\n\nInterface luxury sombre et dorée, cohérente avec la console web. Disponible en FR/EN/ES. Devises USD et DOP.\n\nL'app ne réplique aucune logique métier : elle est la surface mobile sécurisée de votre ERP OTO Enterprise OS.",
"keywords": "immobilier,ERP,CRM,ventes,construction,chantier,compta,portail,RBAC,OTO"
},
"en": {
"subtitle": "Business portals on the go",
"short_description": "The companion app for your OTO ERP: 5 role-secured business portals.",
"full_description": "OTO Enterprise OS is the mobile companion app for the OTO real-estate platform.\n\nAccess your 5 business portals — OTO Ventes · OTO Construction · OTO Achat · OTO Compta · OTO Direction — right from your phone.\n\nRole-based security (RBAC): each portal only exposes the features your profile is allowed to use; no data outside your scope.\n\nLuxury dark-and-gold interface, consistent with the web console. Available in FR/EN/ES. USD and DOP currencies.\n\nThe app replicates no business logic: it is the secure mobile surface of your OTO Enterprise OS ERP.",
"keywords": "real estate,ERP,CRM,sales,construction,site,accounting,portal,RBAC,OTO,property"
},
"es": {
"subtitle": "Tus portales en movilidad",
"short_description": "La app complementaria de tu ERP OTO: 5 portales seguros por rol.",
"full_description": "OTO Enterprise OS es la aplicación móvil complementaria de la plataforma inmobiliaria OTO.\n\nAccede a tus 5 portales de negocio — OTO Ventes · OTO Construction · OTO Achat · OTO Compta · OTO Direction — desde tu teléfono.\n\nSeguridad por rol (RBAC): cada portal solo muestra las funciones que tu perfil tiene permitidas; ningún dato fuera de tu alcance.\n\nInterfaz luxury oscura y dorada, coherente con la consola web. Disponible en FR/EN/ES. Monedas USD y DOP.\n\nLa app no replica ninguna lógica de negocio: es la superficie móvil segura de tu ERP OTO Enterprise OS.",
"keywords": "inmobiliaria,ERP,CRM,ventas,construcción,obra,contabilidad,portal,RBAC,OTO"
}
}
}
@@ -1,10 +1,13 @@
"""Tests du générateur de config app mobile (Sprint 5 · Mobile).
Stdlib pur (unittest) — aucune dépendance pip. Couvre :
- conformité au schéma + les 12 invariants du CLI (chemin nominal) ;
- conformité au schéma + les 15 invariants du CLI (chemin nominal) ;
- la cross-cohérence RBAC ↔ portails ↔ seo (onglets, rôles, labels/icônes, langues) ;
- la fiche store dérivée (langues seo, limites de caractères, portails/langues
injectés depuis la surface réelle) ;
- l'anti-invention (#6) par injections négatives (identifiant de store fabriqué,
rôles désynchronisés de la surface RBAC, langue inventée, token de marque altéré).
rôles désynchronisés de la surface RBAC, langue inventée, token de marque altéré,
description store dépassant la limite ou désalignée des portails réels).
"""
from __future__ import annotations
@@ -113,6 +116,38 @@ class TestNominal(unittest.TestCase):
self.assertEqual(a, b)
class TestStoreListing(unittest.TestCase):
"""Fiche store : couverture langues, limites de caractères, injection sourcée."""
def setUp(self):
self.bundle = build()
self.sl = self.bundle["store_listing"]
def test_langues_egal_seo(self):
langs, _ = deps.load_langs()
self.assertEqual(sorted(self.sl["content"].keys()), sorted(langs))
self.assertEqual(self.sl["langues"], list(langs))
def test_limites_caracteres_store(self):
spec = deps.load_spec()
limits = spec["store_listing"]["limits"]
for lang, fields in self.sl["content"].items():
for champ, limit in limits.items():
self.assertTrue(fields.get(champ), f"{lang}.{champ} vide")
self.assertLessEqual(len(fields[champ]), limit, f"{lang}.{champ} > {limit}")
def test_portails_et_langues_injectes(self):
"""Anti-dérive : la copie cite les portails/langues RÉELS, pas une liste figée."""
langs, _ = deps.load_langs()
nav = self.bundle["role_navigation"]
portails_str = " · ".join(n["label"] for n in nav)
langues_str = "/".join(l.upper() for l in langs)
for lang, fields in self.sl["content"].items():
self.assertIn(portails_str, fields["full_description"])
self.assertIn(langues_str, fields["full_description"])
self.assertNotIn("{", fields["full_description"])
class TestAntiInvention(unittest.TestCase):
"""Injections négatives : chaque garde-fou doit MORDRE (invariant → erreur)."""
@@ -149,6 +184,18 @@ class TestAntiInvention(unittest.TestCase):
errs = gen._validate_bundle(bad)
self.assertTrue(any("ordre des onglets" in e for e in errs), errs)
def test_fiche_store_trop_longue_detecte(self):
bad = copy.deepcopy(self.bundle)
bad["store_listing"]["content"]["fr"]["subtitle"] = "x" * 200
errs = gen._validate_bundle(bad)
self.assertTrue(any("limite store" in e for e in errs), errs)
def test_fiche_store_portails_desalignes_detecte(self):
bad = copy.deepcopy(self.bundle)
bad["store_listing"]["content"]["en"]["full_description"] = "No portals listed here."
errs = gen._validate_bundle(bad)
self.assertTrue(any("full_description" in e for e in errs), errs)
if __name__ == "__main__":
unittest.main()