"""Construction du bundle de config app mobile depuis les sources committées. Entrées : - contract : rbac_50_roles.json (portails_business + rôles · source de vérité des ONGLETS et de leurs rôles autorisés). - spec : mobile_spec.json (choix de config : nom/slug/scheme, profils EAS, ordre des onglets, liste des champs `a_confirmer`). - portails_spec : labels + icônes des 5 portails (mise en page · même source que les Workspaces). - langs : langues FR/EN/ES + défaut (seo_spec · source unique). Sortie : bundle déterministe (tri stable, aucun horodatage) : - app_config -> objet Expo `app.config` (thème dark+doré #4, locales seo, identifiants de store `null` · a_confirmer). - eas_build -> objet `eas.json` (profils de build + submit, credentials `null`). - role_navigation -> 1 onglet par portail métier, gated par la surface RBAC exacte. - manifest -> traçabilité (comptes, résumé par portail, marque, a_confirmer). Anti-invention (#6) : aucun identifiant de store/build fabriqué (tous `null`) ; rôles = surface RBAC exacte du portail (mêmes valeurs que les Has Role des Workspaces) ; tokens de marque + devises + langues repris verbatim de leurs sources uniques. """ from __future__ import annotations from typing import Any from . import deps def _labels_icons(portails_spec: dict) -> tuple[dict, dict]: labels = {p["key"]: p["label"] for p in portails_spec["portails"]} icons = {p["key"]: p["icon"] for p in portails_spec["portails"]} return labels, icons def build_app_config(spec: dict, langs: list[str], default_lang: str) -> dict[str, Any]: """Objet Expo `app.config`. Thème #4 ; identifiants de store `null` (#6/#8).""" app = spec["app"] b = deps.branding return { "expo": { "name": app["name"], "slug": app["slug"], "scheme": app["scheme"], "version": None, "orientation": app["orientation"], "userInterfaceStyle": app["user_interface_style"], "backgroundColor": b.COLOR_BG, "primaryColor": b.COLOR_ACCENT, "icon": None, "splash": { "backgroundColor": b.COLOR_BG, "resizeMode": "contain", "image": None, }, "locales": {lang: None for lang in langs}, "ios": { "bundleIdentifier": None, "buildNumber": None, "supportsTablet": True, }, "android": { "package": None, "versionCode": None, }, "extra": { "expoSdkMajor": app["expo_sdk_major"], "defaultLocale": default_lang, "devises": [b.DEVISE_PRIMAIRE, b.DEVISE_SECONDAIRE], "eas": {"projectId": None}, }, "plugins": [], } } def build_eas(spec: dict) -> dict[str, Any]: """Objet `eas.json` : profils de build + submit ; credentials `null` (#6/#8).""" return { "cli": {"version": None, "appVersionSource": spec["eas"]["app_version_source"]}, "build": { "development": { "developmentClient": True, "distribution": "internal", "channel": "development", }, "preview": {"distribution": "internal", "channel": "preview"}, "production": {"channel": "production", "autoIncrement": True}, }, "submit": { "production": { "ios": {"appleId": None, "ascAppId": None, "appleTeamId": None}, "android": {"serviceAccountKeyPath": None, "track": "internal"}, } }, } def build_navigation(contract: dict, spec: dict, portails_spec: dict) -> list[dict[str, Any]]: """1 onglet par portail métier, dans l'ordre de la spec ; rôles = surface RBAC exacte.""" roles_map = deps.roles_by_portail(contract) labels, icons = _labels_icons(portails_spec) nav: list[dict[str, Any]] = [] for key in spec["portail_order"]: roles = sorted(roles_map.get(key, [])) nav.append( { "portail": key, "label": labels[key], "icon": icons[key], "route": f"/{key}", "roles_allowed": roles, "n_roles": len(roles), } ) 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 = [ { "portail": n["portail"], "label": n["label"], "icon": n["icon"], "nb_roles": n["n_roles"], } for n in nav ] manifest = { "generated_from": spec["generated_from"], "spec_version": spec["version"], "source_version": contract["version"], "roadmap_ref": spec["roadmap_ref"], "app": { "name": spec["app"]["name"], "slug": spec["app"]["slug"], "scheme": spec["app"]["scheme"], "expo_sdk_major": spec["app"]["expo_sdk_major"], }, "counts": { "onglets": len(nav), "portails": len(nav), "roles_couverts": sum(n["n_roles"] for n in nav), "langues": len(langs), "identifiants_a_confirmer": len(spec["a_confirmer"]), }, "brand": { "fond": {"valeur": b.COLOR_BG, "source": "CLAUDE.md #4"}, "accent": {"valeur": b.COLOR_ACCENT, "source": "CLAUDE.md #4"}, }, "devises": { "primaire": b.DEVISE_PRIMAIRE, "secondaire": b.DEVISE_SECONDAIRE, "source": "CLAUDE.md #10", }, "langues": { "liste": list(langs), "defaut": default_lang, "source": "seo/seo_spec.json · site.langs", }, "a_confirmer": spec["a_confirmer"], "identifiants_statut": "a_confirmer", "portails": portails_meta, "hand_off": ( "Build Expo 54 + submit App Store #32 / Play Store exécutés hors de ce " "worker (agent Mobile · #8) : ce livrable ne produit que la config " "versionnable. Les identifiants/credentials `null` sont renseignés au " "build sur le poste/CI Mobile, jamais fabriqués ici (#6)." ), } return { "app_config": app_config, "eas_build": eas_build, "role_navigation": nav, "store_listing": store_listing, "manifest": manifest, }