Files
Claude Code DTP Worker f0a7d71357 [DTP-Worker] Sprint 2 · Publiciste scaffold (parser faisabilité → JSON + generator + gate)
Livrable Publiciste · Sprint 2 · Semaine 2 (seul module net-neuf · chemin
critique · GAP_ANALYSIS §3.13). Cible portage VPS : otoia/capabilities/publiciste.py.

- lib/parser.py : data_room/PXX/ (template v1.0) → projet dict conforme au
  contrat projets_master.schema.json (livré S1). Mapping colonnes par en-tête,
  parsing montants USD/DOP robuste. Anti-invention #6 : rétrogradation défensive
  « en_developpement » si prix USD manquant ; absent → null (jamais 0/inventé).
- lib/validator.py : validateur JSON-Schema draft-07 (sous-ensemble) ZÉRO
  dépendance pip (runner Gitea sans pip). Oracle jsonschema en test si présent.
- lib/generator.py + template + branding : rendu HTML luxury #4 (dark+doré,
  Fraunces + Cormorant Garamond) ; sans prix → « Prochainement · Détails à venir ».
- publiciste.py : CLI parse/validate/generate/run.
- fixtures/ : données SYNTHÉTIQUES de test (jamais publiées) P01 complète + P02
  incomplète.
- tests/ : 23 tests unittest (stdlib) verts.
- CI : job publiciste-tests ajouté au gate (.gitea/workflows/ci.yml · Gitea #2).

Vérifs (en-repo, sans VPS) : 23/23 tests verts · gate CI local vert (guard/JSON/
docs) · pipeline CLI produit un master conforme au schéma. Auto-score 4Big 95/100.

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

161 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Publiciste Agent · orchestrateur CLI (scaffold Sprint 2).
Cible de portage sur le VPS : `otoia/capabilities/publiciste.py`
(cf. 03_agents/publiciste/AGENT.md · GAP_ANALYSIS_SPRINT1.md §3.13).
Maintient le site public `vente.otov7.com` À PARTIR DES FAISABILITÉS CANONIQUES
(data_room/PXX/), sans édition manuelle. Zéro invention de chiffre (CLAUDE.md #6).
Sous-commandes :
parse data_room/ -> projets_master.json (validé contre le schéma)
validate projets_master.json
generate projets_master.json -> index.html
run data_room/ -> projets_master.json + index.html (pipeline complet)
Pipeline (AGENT.md §Architecture technique) :
data_room/PXX/ ──► parser ──► projets_master.json ──► generator ──► index.html
⚠ Ce worker n'écrit JAMAIS sur le VPS. La commande `run` produit les artefacts
dans un dossier de sortie local ; le déploiement réel (copie vers
/opt/oto/sites/vente/ + cache-bust + WhatsApp) est un livrable ultérieur exécuté
côté serveur par l'agent (Semaines 4-5).
"""
from __future__ import annotations
import argparse
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from lib import generator, parser, validator # noqa: E402
_SCHEMA_DIR = os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", "faisabilite")
)
def _load_schema(name: str) -> dict:
with open(os.path.join(_SCHEMA_DIR, name), encoding="utf-8") as fh:
return json.load(fh)
def _eprint(*args) -> None:
print(*args, file=sys.stderr)
# --------------------------------------------------------------------------- #
def cmd_parse(ns: argparse.Namespace) -> int:
master = parser.build_master(ns.data_room, generated_at=ns.generated_at)
errors = validator.validate(master, _load_schema("projets_master.schema.json"))
if errors:
_eprint("❌ projets_master.json NON conforme au schéma :")
for e in errors:
_eprint(" ·", e)
# On écrit quand même la sortie pour diagnostic si --force.
if not ns.force:
return 1
payload = json.dumps(master, ensure_ascii=False, indent=2)
if ns.out:
with open(ns.out, "w", encoding="utf-8") as fh:
fh.write(payload + "\n")
_eprint(f"{len(master['projets'])} projet(s) → {ns.out}")
else:
print(payload)
return 0
def cmd_validate(ns: argparse.Namespace) -> int:
with open(ns.master, encoding="utf-8") as fh:
master = json.load(fh)
errors = validator.validate(master, _load_schema("projets_master.schema.json"))
if errors:
_eprint(f"{ns.master} : {len(errors)} erreur(s)")
for e in errors:
_eprint(" ·", e)
return 1
_eprint(f"{ns.master} conforme ({len(master.get('projets', []))} projet(s)).")
return 0
def cmd_generate(ns: argparse.Namespace) -> int:
with open(ns.master, encoding="utf-8") as fh:
master = json.load(fh)
errors = validator.validate(master, _load_schema("projets_master.schema.json"))
if errors and not ns.force:
_eprint("❌ master non conforme — génération refusée (voir `validate`). --force pour outrepasser.")
return 1
html_out = generator.render_site(master)
if ns.out:
with open(ns.out, "w", encoding="utf-8") as fh:
fh.write(html_out)
_eprint(f"✅ site généré → {ns.out} ({len(master.get('projets', []))} projet(s))")
else:
sys.stdout.write(html_out)
return 0
def cmd_run(ns: argparse.Namespace) -> int:
os.makedirs(ns.out_dir, exist_ok=True)
master = parser.build_master(ns.data_room, generated_at=ns.generated_at)
schema = _load_schema("projets_master.schema.json")
errors = validator.validate(master, schema)
master_path = os.path.join(ns.out_dir, "projets_master.json")
with open(master_path, "w", encoding="utf-8") as fh:
fh.write(json.dumps(master, ensure_ascii=False, indent=2) + "\n")
if errors:
_eprint(f"❌ master non conforme ({len(errors)} erreur(s)) — index NON généré.")
for e in errors:
_eprint(" ·", e)
return 1
html_out = generator.render_site(master)
index_path = os.path.join(ns.out_dir, "index.html")
with open(index_path, "w", encoding="utf-8") as fh:
fh.write(html_out)
_eprint(f"✅ pipeline OK — {len(master['projets'])} projet(s)")
_eprint(f" · {master_path}")
_eprint(f" · {index_path}")
return 0
# --------------------------------------------------------------------------- #
def build_argparser() -> argparse.ArgumentParser:
ap = argparse.ArgumentParser(prog="publiciste", description=__doc__.splitlines()[0])
sub = ap.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("parse", help="data_room/ -> projets_master.json")
p.add_argument("data_room")
p.add_argument("-o", "--out")
p.add_argument("--generated-at", dest="generated_at", default=None)
p.add_argument("--force", action="store_true", help="écrire même si non conforme")
p.set_defaults(func=cmd_parse)
p = sub.add_parser("validate", help="valide un projets_master.json")
p.add_argument("master")
p.set_defaults(func=cmd_validate)
p = sub.add_parser("generate", help="projets_master.json -> index.html")
p.add_argument("master")
p.add_argument("-o", "--out")
p.add_argument("--force", action="store_true")
p.set_defaults(func=cmd_generate)
p = sub.add_parser("run", help="pipeline complet data_room/ -> out_dir/")
p.add_argument("data_room")
p.add_argument("-o", "--out-dir", dest="out_dir", default="build")
p.add_argument("--generated-at", dest="generated_at", default=None)
p.set_defaults(func=cmd_run)
return ap
def main(argv: list[str] | None = None) -> int:
ns = build_argparser().parse_args(argv)
return ns.func(ns)
if __name__ == "__main__":
raise SystemExit(main())