Files
oto-enterprise-os-dtp/05_deliverables_mvp/frontend/chat_otoia/chatlib/builder.py
T
Claude Code DTP Worker 17ad5c0a01
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 / 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 / E2E baseline Playwright (manuel) (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled
[DTP-Worker] Sprint 6 · Générateur Chat OTOIA embarqué par portail (Custom Block natif) (ERPNext Backend · roadmap L63)
Carrier natif ERPNext v15 (Custom Block) + config runtime par portail.
Ancrage RBAC : roles_allowed/knowledge_scope = surface exacte du portail,
synchronisés avec les Has Role des Workspaces. Endpoint OTOIA null (a_confirmer).
Persona Amélie + capabilities + langues FR/EN/ES sourcés (anti-invention #6).
14 invariants · 31 tests · régression 408 tests verts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 09:37:51 +00:00

139 lines
5.8 KiB
Python

"""Assemblage du bundle Chat OTOIA depuis (contrat RBAC + spec + portails + seo).
Sortie déterministe :
- custom_block : 1 fixture Frappe `Custom Block` par portail (le carrier natif du
chat, inséré dans le `content` du Workspace du portail côté VPS) ;
- chat_mount : 1 config runtime par portail (persona, langues, rôles autorisés,
portée de connaissance, capabilities, endpoint=null) — ce que le runtime OTOIA
consomme ; données pures, diffables ;
- manifest : traçabilité (comptes, résumé par portail, marque, hand-off VPS).
Anti-invention (#6), garanti ici + par les invariants du CLI :
- portails = EXACTEMENT les portails métier (portails_spec / portails_business),
la console technique `plateforme` étant exclue (comme les Workspaces) ;
- `roles_allowed` / `knowledge_scope` viennent du CONTRAT (surface RBAC du portail),
jamais de la spec — mêmes valeurs que les Has Role des Workspaces ;
- `endpoint` reste null (a_confirmer · VPS) — jamais fabriqué ;
- persona / capabilities / langues repris de sources sourcées (CLAUDE.md, seo_spec),
jamais inventés ; aucun chiffre émis.
Déterminisme : portails dans l'ordre de la spec ; rôles + DocTypes triés → sortie
reproductible bit-à-bit (aucun horodatage, aucun aléatoire).
"""
from __future__ import annotations
from typing import Any
from . import deps, frappe, knowledge
def _portail_labels(portails_spec: dict) -> dict[str, str]:
return {p["key"]: p["label"] for p in portails_spec["portails"]}
def build_bundle(
contract: dict,
spec: dict,
portails_spec: dict,
langs: list[str],
default_lang: str,
) -> dict[str, Any]:
"""Transforme les entrées en bundle {custom_block, chat_mount, manifest}."""
labels = _portail_labels(portails_spec)
keys = [p["key"] for p in portails_spec["portails"]] # ordre spec = déterminisme
mount_cfg = spec["mount"]
id_prefix = mount_cfg["html_id_prefix"]
css_class = mount_cfg["container_class"]
bn_prefix = mount_cfg["block_name_prefix"]
persona = {"nom": spec["persona"]["nom"], "voix": spec["persona"]["voix"]}
capabilities = [c["module"] for c in spec["capabilities"]]
ui_label = dict(spec["ui_label"])
custom_blocks: list[dict] = []
mounts: list[dict] = []
manifest_portails: list[dict] = []
for key in keys:
label = labels[key]
roles = knowledge.roles_for(contract, key)
scope = knowledge.knowledge_scope_for(contract, key)
html_id = f"{id_prefix}{key}"
block_name = f"{bn_prefix}{label}"
html = frappe.mount_html(html_id, key, persona["nom"], css_class)
custom_blocks.append(frappe.custom_block_fixture(block_name, html))
mounts.append({
"portail": key,
"workspace": label,
"custom_block": block_name,
"html_id": html_id,
"persona": persona,
"langues": list(langs),
"lang_defaut": default_lang,
"ui_label": ui_label,
"roles_allowed": roles,
"knowledge_scope": scope,
"capabilities": list(capabilities),
"endpoint": None,
})
manifest_portails.append({
"portail": key,
"workspace": label,
"custom_block": block_name,
"nb_roles": len(roles),
"nb_knowledge_doctypes": len(scope),
})
knowledge_union = sorted({dt for m in mounts for dt in m["knowledge_scope"]})
manifest = {
"deliverable": spec["deliverable"],
"sprint": spec["sprint"],
"roadmap_ref": spec["roadmap_ref"],
"generated_from": spec["generated_from"],
"carrier": {
"doctype": frappe.CUSTOM_BLOCK_DOCTYPE,
"workspace_content_block_type": frappe.WORKSPACE_CONTENT_BLOCK_TYPE,
"note": "Chaque Custom Block est inséré dans le `content` (editor.js) du "
"Workspace de son portail via un bloc `custom_block` référençant "
"`block_name`. La clé exacte du payload editor.js est a_confirmer "
"selon le patch de version v15 — étape VPS (agent Frontend).",
},
"persona": persona,
"langues": list(langs),
"lang_defaut": default_lang,
"capabilities": list(capabilities),
"endpoint_statut": spec["endpoint"]["statut"],
"counts": {
"portails": len(mounts),
"custom_blocks": len(custom_blocks),
"mounts": len(mounts),
"roles_couverts": sum(m["nb_roles"] for m in manifest_portails),
"knowledge_doctypes_uniques": len(knowledge_union),
},
"portails": manifest_portails,
"brand": {
"fond": {"valeur": deps.branding.COLOR_BG, "source": "CLAUDE.md #4"},
"accent": {"valeur": deps.branding.COLOR_ACCENT, "source": "CLAUDE.md #4"},
"polices": [
{"valeur": deps.branding.FONT_DISPLAY, "source": "CLAUDE.md #4"},
{"valeur": deps.branding.FONT_BODY, "source": "CLAUDE.md #4"},
],
},
"handoff_vps": [
"Créer/confirmer le module propriétaire OTO côté app puis importer les "
"fixtures `Custom Block` (bench).",
"Insérer dans le `content` de chaque Workspace (frontend/portails/out/"
"workspace.json) un bloc `custom_block` référençant `block_name` (payload "
"editor.js a_confirmer selon version).",
"Renseigner l'endpoint OTOIA (desk/bim-cloud) dans la config runtime + "
"charger le web-component OTOIA via le thème desk (agent Frontend · #8).",
"Appliquer les tokens de marque dark+doré (#4) au montage via le thème.",
],
}
return {"custom_block": custom_blocks, "chat_mount": mounts, "manifest": manifest}