[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
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:
@@ -37,7 +37,7 @@ le VPS reçoit — jamais l'inverse.
|
||||
vente, le barème commissions, le DocType Dossier Vente et le plan e-CF **résolvent**
|
||||
leurs rôles depuis ce fichier, jamais un nom Frappe en dur. Total backend **RBAC 60
|
||||
tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests**, tous gated dans le CI (matrice
|
||||
de régression du repo : **564 tests · 22 suites** · verdict `PASS`, source
|
||||
de régression du repo : **569 tests · 22 suites** · verdict `PASS`, source
|
||||
`qa/regression/out/regression_run.json` — jamais compté à la main · #6).
|
||||
|
||||
## Hand-off reçus (à appliquer sur le VPS, dans l'ordre)
|
||||
|
||||
@@ -32,7 +32,7 @@ périmètre — un auditeur ne s'audite jamais.
|
||||
**conformité** normative des hand-off.
|
||||
|
||||
## Verdict agrégé courant (source `qa/regression/out/regression_run.json`)
|
||||
**22 suites gated · 564 tests · 564 passés · 0 échec · 0 erreur → verdict `PASS`.**
|
||||
**22 suites gated · 569 tests · 569 passés · 0 échec · 0 erreur → verdict `PASS`.**
|
||||
Ce compte est **découvert automatiquement depuis le CI** (`q4lib.registry.parse_ci`,
|
||||
réutilisé sans duplication) — jamais compté à la main.
|
||||
|
||||
|
||||
@@ -93,3 +93,39 @@ surfaces inter-artefacts neuves auditées sans dérive + reporting canal 2 (aucu
|
||||
livrable net nouveau — attendu en buffer Sprint 8 saturé) ; déploiement serveur hors
|
||||
périmètre (VPS · #8). État launch-readiness **inchangé et intégralement sourcé** :
|
||||
22/22 qualité · 564/564 régression · 15/15 recette · 30 gate.needs verts.
|
||||
|
||||
---
|
||||
|
||||
## Addendum · Session `20260803_030641` · Sprint 5 Mobile — fiche store FR/EN/ES (livrable net)
|
||||
|
||||
**Tâche exécutée (livrable fonctionnel, périmètre worker).** Ajout de la **fiche store
|
||||
App Store / Play** au livrable Mobile in-repo
|
||||
([`mobile/app_config`](../mobile/app_config/)) — l'un des **non-bloqueurs** explicites
|
||||
de `DIRECTIVE_MOBILE_STORES_20260803.md` (P3 · « Contenus descriptions · rédiger
|
||||
FR/EN/ES »), sans EAS_TOKEN ni credentials (attendus de Michel · vrais bloqueurs).
|
||||
|
||||
**Design anti-invention (#6).** Nouvel artefact
|
||||
[`out/store_listing.json`](../mobile/app_config/out/store_listing.json) (sous-titre,
|
||||
description courte/longue, mots-clés · FR/EN/ES). Les **faits produit** driftables —
|
||||
nombre + énumération des portails, langues — sont **INJECTÉS** depuis la surface réelle
|
||||
(`role_navigation` = labels `portails_spec`, langues `seo_spec`) au build ; seuls le
|
||||
style rédactionnel et les mots-clés sont authoriaux (même catégorie que `app.name`).
|
||||
La copie marketing ne peut donc **pas mentir** sur les portails/langues (invariant 14).
|
||||
|
||||
**3 nouveaux invariants** (12 fiche=langues seo · 13 limites de caractères store
|
||||
App Store/Play · 14 injection portails/langues == surface réelle) → **12→15
|
||||
invariants** (déterminisme = 15). Schéma étendu (`store_listing` + `$ref`
|
||||
`store_listing_lang`). 5 tests ajoutés (22 méthodes mobile · +5 → **569** au total).
|
||||
|
||||
**Consommateurs régénérés (byte-repro #artifact-gate).** `qa/audit_4big` (22/22 · min
|
||||
100) · `qa/regression` plan+run (**569** tests) · fiches gatées `03_agents/qa` &
|
||||
`03_agents/erpnext_backend` (564→569). `./run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP**.
|
||||
|
||||
| Dimension | Valeur (après cette session) | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Régression (run) | **22 suites · 569 ran · 569 passés · 0 échec · 0 erreur · 0 skip** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Qualité 4Big | **22/22 modules à 100/100** (seuil 95) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Fiche store Mobile | **FR/EN/ES** · portails/langues injectés · limites store validées | `PASS` (15 invariants) | [`mobile/app_config/out/store_listing.json`](../mobile/app_config/out/store_listing.json) |
|
||||
|
||||
**Hors périmètre (inchangé · #8).** `eas build`/`eas submit`, assets binaires
|
||||
(icône/splash 1024²), credentials Apple/Google → agent Mobile (VPS/stores).
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -606,7 +606,7 @@
|
||||
"criterion": "DOC",
|
||||
"weight": 20,
|
||||
"passed": true,
|
||||
"evidence": "README.md (3703 octets)"
|
||||
"evidence": "README.md (4261 octets)"
|
||||
},
|
||||
{
|
||||
"criterion": "CONTRAT",
|
||||
@@ -618,7 +618,7 @@
|
||||
"criterion": "TESTS",
|
||||
"weight": 25,
|
||||
"passed": true,
|
||||
"evidence": "17 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
||||
"evidence": "22 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
||||
},
|
||||
{
|
||||
"criterion": "CLI",
|
||||
@@ -630,7 +630,7 @@
|
||||
"criterion": "HANDOFF",
|
||||
"weight": 20,
|
||||
"passed": true,
|
||||
"evidence": "MANIFEST + 3 artefact(s) JSON valides"
|
||||
"evidence": "MANIFEST + 4 artefact(s) JSON valides"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"version": "1.0",
|
||||
"verdict": "PASS",
|
||||
"suites": 22,
|
||||
"test_methods": 564,
|
||||
"test_methods": 569,
|
||||
"coverage_ok": true,
|
||||
"artifacts": [
|
||||
"regression_plan.json"
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"in_gate": true,
|
||||
"has_tests_dir": true,
|
||||
"test_files": 1,
|
||||
"test_methods": 17
|
||||
"test_methods": 22
|
||||
},
|
||||
{
|
||||
"id": "publiciste",
|
||||
@@ -273,7 +273,7 @@
|
||||
"totals": {
|
||||
"suites": 22,
|
||||
"test_files": 22,
|
||||
"test_methods": 564,
|
||||
"test_methods": 569,
|
||||
"min_methods": 10,
|
||||
"under_threshold": 0
|
||||
},
|
||||
|
||||
@@ -138,9 +138,9 @@
|
||||
{
|
||||
"id": "mobile.app_config",
|
||||
"path": "mobile/app_config",
|
||||
"expected_methods": 17,
|
||||
"ran": 17,
|
||||
"passed": 17,
|
||||
"expected_methods": 22,
|
||||
"ran": 22,
|
||||
"passed": 22,
|
||||
"failures": 0,
|
||||
"errors": 0,
|
||||
"skipped": 0,
|
||||
@@ -272,8 +272,8 @@
|
||||
"suites": 22,
|
||||
"green": 22,
|
||||
"red": 0,
|
||||
"ran": 564,
|
||||
"passed": 564,
|
||||
"ran": 569,
|
||||
"passed": 569,
|
||||
"failures": 0,
|
||||
"errors": 0,
|
||||
"skipped": 0
|
||||
|
||||
Reference in New Issue
Block a user