catalog-core (0.2.0)
Installation
pip install --index-url catalog-coreAbout this package
Brique réutilisable offre & catalogue par marque (CDC-CATALOG-CORE-v1) — embarquée par chaque app, données par app, claims gouvernés C3.
catalog-core
Brique logicielle réutilisable — gestion de l'offre commerciale : le catalogue par marque (quels produits sont offerts, à quel prix, où, et avec quels claims) + contrat de consolidation vers le Cockpit.
Spécifiée par CDC-CATALOG-CORE-v1 (Holding MAPFARM). 3ᵉ membre du trio commercial —
client-core (à qui on vend) · catalog-core (quoi on vend) · fin-core
(l'argent) — reliés par l'ordre (OMS, CDC-3). Chaque application embarque la
brique et possède ses propres données (store par app) ; le Cockpit consolide une vue
holding par read-model et n'écrit jamais dans les stores des apps.
Ce dépôt = Livrable 0 du CDC : la brique seule (modèle d'offre §5, claims gouvernés C3 §7, garde-fous de publication §6.1, contrat de consolidation §3.3). Le câblage consommateur (Harvest L1, Cockpit L2, multi-marque/structure L3, promotions/multi-canal L4) est hors de ce dépôt.
Principes (non négociables — CDC §8)
| # | Règle |
|---|---|
| R1 | Tout objet catalogue de tête tagué entite_id + societe_id + marque_id (canon, ADR-041). |
| R2 | Chaque app est propriétaire de son catalogue (séparabilité). |
| R3 | Aucun store catalogue partagé en écriture ; Cockpit = read-model. |
| R4 | Le produit est défini en CDC-4 ; l'offre référence (produit_ref), ne le réinvente pas. |
| R5 | Tout claim publié passe le pare-feu C3 — grounded sinon bloqué (fail-closed). |
| R6 | Claim bloqué persisté (audit). |
| R7 | Prix-liste (catalogue) ≠ revenu réalisé (fin-core) — distincts, liés par l'ordre. |
| R8 | Contrat de consolidation standardisé : toute app sur la brique émet le même format. |
Calculs Decimal-first (ROUND_HALF_UP, 2 décimales), HT canonique, EUR.
Claims gouvernés C3 — le cœur de la brique (§7)
Tout claim publié passe le pare-feu épistémique C3 : le grounding compare contre les valeurs affirmées structurées du corpus, jamais la présence textuelle (leçon « arabica » : bloqué même mentionné, car le produit est Fine Robusta). Fail-closed : un claim non fondé est bloqué, jamais publié.
La brique ne dépend pas en dur de corpus : elle expose un port (ClaimGrounder) que
l'app branche sur le moteur réel (corpus_core.fusion_c3). Sans grounder injecté, le défaut
FailClosedGrounder bloque tout.
from catalog_core import Claim, govern_claim, VerdictC3
class CorpusGrounder: # implémentation app-côté, adossée au corpus (T1)
def ground(self, claim): ... # -> GroundingVerdict(grounded=..., blocked=..., ...)
claim = Claim(id="c1", offre_id="o1", classe="variety", texte="Fine Robusta",
attribut="variety", valeur="Fine Robusta")
jugé = govern_claim(claim, CorpusGrounder())
assert jugé.verdict_c3 in (VerdictC3.GROUNDED, VerdictC3.BLOCKED, VerdictC3.PENDING)
# `jugé.claim_blocked` est persisté (audit).
Installation (registre PyPI Gitea)
--extra-index-url (et non --index-url) : la brique vient du registre Gitea, ses
dépendances (pydantic, psycopg…) restent résolues depuis PyPI.
pip install --extra-index-url https://git.mapfarm.cloud/api/packages/mapfarm/pypi/simple/ catalog-core
# avec l'adaptateur de persistance psycopg :
pip install --extra-index-url https://git.mapfarm.cloud/api/packages/mapfarm/pypi/simple/ "catalog-core[db]"
Nature d'offre — produit ou prestation (0.2.0)
Une offre a une nature :
produit(défaut) — un produitCDC-4rendu disponible à la vente (storefront/PIM) :produit_refrequis (R4), peut porter des claims C3.prestation— un service offert (ex. le catalogue de services de la Factory) : pas deproduit_ref, généralement sans claim.
Usage
from decimal import Decimal
from catalog_core import Offre, Prix, CatalogRules, NatureOffre
# Offre produit (storefront) — produit_ref obligatoire (R4)
offre = Offre(id="o1", entite_id="harvest", societe_id="mapfarm", marque_id="mlc",
produit_ref="prod-magellan")
prix = Prix(id="p1", offre_id="o1", montant=Decimal("12.00"), regime_tva="franchise")
# Offre prestation (service) — pas de produit_ref
service = Offre(id="svc-dev", entite_id="it", societe_id="mapfarm", marque_id="factory",
nature=NatureOffre.PRESTATION)
tarif = Prix(id="t-dev", offre_id="svc-dev", montant=Decimal("600.00")) # forfait
rules = CatalogRules()
publiee = rules.publish(offre, claims=[]) # refuse si un claim est bloqué (R5)
Persistance (chaque app, sur son schéma) :
import psycopg
from catalog_core.schema import apply_schema
with psycopg.connect(dsn, autocommit=True) as conn:
apply_schema(conn, "xavier__harvest__catalog") # DDL idempotent, store de l'app
Consolidation (read-model émis vers le Cockpit) :
from catalog_core.consolidation import build_report
report = build_report(app="harvest", offres=offres, prix=prix,
disponibilites=dispo, claims=claims) # par marque + taux de claims bloqués
Dev
make install # pip install -e ".[test]"
make lint # ruff
make test # pytest (DDL idempotent skip si pas de Docker)
make build # sdist + wheel