0d3b2420c3
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 / E2E baseline Playwright (manuel) (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled
84 lines
2.8 KiB
Python
84 lines
2.8 KiB
Python
"""Dependances partagees du generateur SEO · reutilisation stricte (#5).
|
|
|
|
On NE reimplemente rien qui existe deja dans le mandat :
|
|
- `validator` (validateur JSON-Schema draft-07 maison, zero pip) et
|
|
- `branding` (tokens de marque #4 + STATUTS_SANS_PRIX / devise #10)
|
|
sont importes du livrable Publiciste, source unique de verite pour ces deux
|
|
briques. Le contrat d'entree consomme est `projets_master.schema.json` (contrat
|
|
Faisabilite<->Publiciste), lui aussi partage.
|
|
|
|
Fonctions locales minimales : chargement des fichiers + `slugify` deterministe
|
|
(pas de dependance externe, pas d'horloge).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import unicodedata
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
_SEO = os.path.dirname(_HERE)
|
|
_DELIVERABLES = os.path.normpath(os.path.join(_SEO, "..")) # 05_deliverables_mvp/
|
|
_PUBLICISTE = os.path.join(_DELIVERABLES, "publiciste")
|
|
_FAISABILITE = os.path.join(_DELIVERABLES, "faisabilite")
|
|
|
|
if _PUBLICISTE not in sys.path:
|
|
sys.path.insert(0, _PUBLICISTE)
|
|
|
|
from lib import branding # type: ignore # noqa: E402
|
|
from lib import validator as maison # type: ignore # noqa: E402
|
|
|
|
# Chemins des contrats/entrees par defaut.
|
|
SPEC_PATH = os.path.join(_SEO, "seo_spec.json")
|
|
SCHEMA_PATH = os.path.join(_SEO, "seo.schema.json")
|
|
MASTER_DEFAULT = os.path.join(_SEO, "fixtures", "projets_master.json")
|
|
MASTER_SCHEMA_PATH = os.path.join(_FAISABILITE, "projets_master.schema.json")
|
|
|
|
|
|
def load_json(path: str) -> dict:
|
|
with open(path, encoding="utf-8") as fh:
|
|
return json.load(fh)
|
|
|
|
|
|
def load_spec() -> dict:
|
|
return load_json(SPEC_PATH)
|
|
|
|
|
|
def load_master(path: str | None = None) -> dict:
|
|
return load_json(path or MASTER_DEFAULT)
|
|
|
|
|
|
def validate_master(master: dict) -> list[str]:
|
|
"""Verifie que l'entree respecte le contrat Publiciste avant tout usage."""
|
|
return list(maison.validate(master, load_json(MASTER_SCHEMA_PATH)))
|
|
|
|
|
|
def validate(instance, schema: dict) -> list[str]:
|
|
return list(maison.validate(instance, schema))
|
|
|
|
|
|
_SLUG_STRIP = re.compile(r"[^a-z0-9]+")
|
|
|
|
|
|
def slugify(text: str) -> str:
|
|
"""Slug URL deterministe : minuscule, sans accent, mots joints par '-'.
|
|
|
|
Conserve les chiffres presents dans la donnee source (ex. « 1069 Crisfer »
|
|
-> « 1069-crisfer ») : on ne retire ni n'invente aucun chiffre (#6)."""
|
|
norm = unicodedata.normalize("NFKD", text)
|
|
norm = "".join(c for c in norm if not unicodedata.combining(c))
|
|
return _SLUG_STRIP.sub("-", norm.lower()).strip("-")
|
|
|
|
|
|
def digits(text: str) -> set[str]:
|
|
"""Ensemble des chiffres presents dans une chaine (audit anti-invention)."""
|
|
return set(re.findall(r"\d", text))
|
|
|
|
|
|
# Re-exports pour les modules soeurs (une seule source d'import).
|
|
STATUTS_SANS_PRIX = branding.STATUTS_SANS_PRIX
|
|
DEVISE_PRIMAIRE = branding.DEVISE_PRIMAIRE
|