Files
oto-enterprise-os-dtp/05_deliverables_mvp/seo/seolib/keywords.py
T
Claude Code DTP Worker 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
[DTP-Worker 20260730_085719] Auto exec · session 20260730_085719
2026-07-30 09:12:19 +00:00

120 lines
4.8 KiB
Python

"""Generation deterministe des mots-cles SEO FR/EN/ES.
PRINCIPE ANTI-INVENTION (#6) : un mot-cle n'est JAMAIS un fait invente. C'est une
composition de :
- tokens factuels issus de `projets_master.json` (nom, localisation) — sourcables ;
- vocabulaire editorial generique du lexique (immobilier / a vendre / ...) —
non chiffre, sans reference a un projet particulier.
Chaque mot-cle porte donc une liste `sources` non vide (soit `lexicon:*`, soit
`projet:<code>.<champ>`), et ne peut contenir que des chiffres deja presents dans
son champ projet source. Cette regle est verifiee par un invariant du generateur.
Sortie : liste triee de dicts {term, lang, scope, projet, intent, category,
sources}. Tri stable (lang, scope, projet, term) -> diffable + re-generable.
"""
from __future__ import annotations
def _confotur_source(projets: list[dict], marker: str) -> str | None:
"""Code du premier projet documentant le regime dans `inclus` (ou None)."""
for p in projets:
if any(marker in (s or "") for s in p.get("inclus", [])):
return p["code"]
return None
def generate(spec: dict, master: dict) -> list[dict]:
site = spec["site"]
lex = spec["lexicon"]
langs = site["langs"]
projets = master["projets"]
property_generic = lex["property_generic"]
property_types = lex["property_types"]
intents = lex["intents"]
intents_for_type = set(lex.get("intents_for_type", [i["key"] for i in intents]))
country = lex["country"]
regime = lex.get("regime")
confotur_code = None
if regime:
confotur_code = _confotur_source(projets, regime["requires_inclus"])
lang_index = {lang: i for i, lang in enumerate(langs)}
acc: list[dict] = []
def add(term, lang, scope, projet, intent, category, sources):
term = " ".join(str(term).split()).strip()
acc.append({
"term": term,
"lang": lang,
"scope": scope,
"projet": projet,
"intent": intent,
"category": category,
"sources": list(sources),
})
for lang in langs:
pg = property_generic[lang]
ctry = country[lang]
# --- Mots-cles GLOBAUX (marque / pays / regime) : independants d'un
# projet mais adosses au lexique editorial (jamais un fait invente).
add(f"{pg} {ctry}", lang, "global", None, None, "generic",
["lexicon:property_generic", "lexicon:country"])
for pt in property_types:
add(f"{pt[lang]} {ctry}", lang, "global", None, None, pt["key"],
["lexicon:property_types", "lexicon:country"])
for it in intents:
add(f"{pg} {it[lang]} {ctry}", lang, "global", None, it["key"], "generic",
["lexicon:property_generic", "lexicon:intents", "lexicon:country"])
if regime and confotur_code:
add(f"{pg} {regime['term'][lang]} {ctry}", lang, "global", None, None,
regime["key"],
["lexicon:regime", "lexicon:country", f"projet:{confotur_code}.inclus"])
# --- Mots-cles PAR PROJET : nom (factuel) + localisation (factuel)
# croises avec le lexique editorial.
for p in projets:
code, nom, loc = p["code"], p["nom"], p["localisation"]
nom_src = f"projet:{code}.nom"
loc_src = f"projet:{code}.localisation"
add(nom, lang, "projet", code, None, None, [nom_src])
add(f"{nom} {ctry}", lang, "projet", code, None, None,
[nom_src, "lexicon:country"])
for it in intents:
add(f"{nom} {it[lang]}", lang, "projet", code, it["key"], None,
[nom_src, "lexicon:intents"])
add(f"{pg} {loc}", lang, "projet", code, None, "generic",
["lexicon:property_generic", loc_src])
for pt in property_types:
add(f"{pt[lang]} {loc}", lang, "projet", code, None, pt["key"],
["lexicon:property_types", loc_src])
for pt in property_types:
for it in intents:
if it["key"] not in intents_for_type:
continue
add(f"{pt[lang]} {it[lang]} {loc}", lang, "projet", code,
it["key"], pt["key"],
["lexicon:property_types", "lexicon:intents", loc_src])
# --- Dedup (term, lang) en conservant la premiere occurrence, puis tri
# deterministe.
seen: set[tuple[str, str]] = set()
deduped: list[dict] = []
for k in acc:
key = (k["lang"], k["term"])
if key in seen:
continue
seen.add(key)
deduped.append(k)
scope_order = {"global": 0, "projet": 1}
deduped.sort(key=lambda k: (
lang_index[k["lang"]], scope_order[k["scope"]], k["projet"] or "", k["term"]
))
return deduped