diff --git a/03_agents/erpnext_backend/AGENT.md b/03_agents/erpnext_backend/AGENT.md index df84de8..1721b39 100644 --- a/03_agents/erpnext_backend/AGENT.md +++ b/03_agents/erpnext_backend/AGENT.md @@ -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) diff --git a/03_agents/qa/AGENT.md b/03_agents/qa/AGENT.md index e2f5aff..171e043 100644 --- a/03_agents/qa/AGENT.md +++ b/03_agents/qa/AGENT.md @@ -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. diff --git a/05_deliverables_mvp/daily_reports/2026-08-03.md b/05_deliverables_mvp/daily_reports/2026-08-03.md index 2c1b744..9360592 100644 --- a/05_deliverables_mvp/daily_reports/2026-08-03.md +++ b/05_deliverables_mvp/daily_reports/2026-08-03.md @@ -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). diff --git a/05_deliverables_mvp/mobile/app_config/README.md b/05_deliverables_mvp/mobile/app_config/README.md index 80d2aeb..0798ca3 100644 --- a/05_deliverables_mvp/mobile/app_config/README.md +++ b/05_deliverables_mvp/mobile/app_config/README.md @@ -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) diff --git a/05_deliverables_mvp/mobile/app_config/app_config_gen.py b/05_deliverables_mvp/mobile/app_config/app_config_gen.py index 3486ea5..b4989bf 100644 --- a/05_deliverables_mvp/mobile/app_config/app_config_gen.py +++ b/05_deliverables_mvp/mobile/app_config/app_config_gen.py @@ -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 diff --git a/05_deliverables_mvp/mobile/app_config/mobile.schema.json b/05_deliverables_mvp/mobile/app_config/mobile.schema.json index 263d280..13bf1f2 100644 --- a/05_deliverables_mvp/mobile/app_config/mobile.schema.json +++ b/05_deliverables_mvp/mobile/app_config/mobile.schema.json @@ -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, diff --git a/05_deliverables_mvp/mobile/app_config/mobile_spec.json b/05_deliverables_mvp/mobile/app_config/mobile_spec.json index c86adaa..978a560 100644 --- a/05_deliverables_mvp/mobile/app_config/mobile_spec.json +++ b/05_deliverables_mvp/mobile/app_config/mobile_spec.json @@ -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).", diff --git a/05_deliverables_mvp/mobile/app_config/mobilelib/builder.py b/05_deliverables_mvp/mobile/app_config/mobilelib/builder.py index 90bbf52..03b92f3 100644 --- a/05_deliverables_mvp/mobile/app_config/mobilelib/builder.py +++ b/05_deliverables_mvp/mobile/app_config/mobilelib/builder.py @@ -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, } diff --git a/05_deliverables_mvp/mobile/app_config/out/store_listing.json b/05_deliverables_mvp/mobile/app_config/out/store_listing.json new file mode 100644 index 0000000..4eb1c81 --- /dev/null +++ b/05_deliverables_mvp/mobile/app_config/out/store_listing.json @@ -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" + } + } +} diff --git a/05_deliverables_mvp/mobile/app_config/tests/test_mobile_app_config.py b/05_deliverables_mvp/mobile/app_config/tests/test_mobile_app_config.py index ae9fbe2..ed1e5d8 100644 --- a/05_deliverables_mvp/mobile/app_config/tests/test_mobile_app_config.py +++ b/05_deliverables_mvp/mobile/app_config/tests/test_mobile_app_config.py @@ -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() diff --git a/05_deliverables_mvp/qa/audit_4big/out/quality_report.json b/05_deliverables_mvp/qa/audit_4big/out/quality_report.json index 137d66f..6fe91df 100644 --- a/05_deliverables_mvp/qa/audit_4big/out/quality_report.json +++ b/05_deliverables_mvp/qa/audit_4big/out/quality_report.json @@ -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" } ] }, diff --git a/05_deliverables_mvp/qa/regression/out/MANIFEST.json b/05_deliverables_mvp/qa/regression/out/MANIFEST.json index c12d499..e431215 100644 --- a/05_deliverables_mvp/qa/regression/out/MANIFEST.json +++ b/05_deliverables_mvp/qa/regression/out/MANIFEST.json @@ -5,7 +5,7 @@ "version": "1.0", "verdict": "PASS", "suites": 22, - "test_methods": 564, + "test_methods": 569, "coverage_ok": true, "artifacts": [ "regression_plan.json" diff --git a/05_deliverables_mvp/qa/regression/out/regression_plan.json b/05_deliverables_mvp/qa/regression/out/regression_plan.json index dbdbf52..d0fd9cc 100644 --- a/05_deliverables_mvp/qa/regression/out/regression_plan.json +++ b/05_deliverables_mvp/qa/regression/out/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 }, diff --git a/05_deliverables_mvp/qa/regression/out/regression_run.json b/05_deliverables_mvp/qa/regression/out/regression_run.json index dbb523e..404c4c5 100644 --- a/05_deliverables_mvp/qa/regression/out/regression_run.json +++ b/05_deliverables_mvp/qa/regression/out/regression_run.json @@ -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