mapfarm

catalog-core (0.3.0)

Published 2026-09-12 12:38:48 +00:00 by xavier

Installation

pip install --index-url https://git.it.iaos.cc/api/packages/mapfarm/pypi/simple/ --extra-index-url https://pypi.org/simple catalog-core

About 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).

Facette métier d'une offre (0.3.0) — pour les apps qui FABRIQUENT

La brique savait décrire ce qu'on vend (nature, prix, disponibilité, claims). Elle ne disait rien de comment on le fabrique : chaque app le réinventait en code. Une offre gratuite d'un consommateur a payé cette absence — trois cycles LLM sur un rail qui aurait dû être déterministe, parce qu'aucune couche ne portait la contrainte.

Une offre peut donc désormais déclarer, en données, ce qui décide de son exécution :

facette ce qu'elle porte
Fabrication rail (déterministe · assisté · multi-agents), graphe, parcours_ref, harnais_defaut / harnais_autorises / choix_client, contraintes non négociables
Cadrage brief (champs rendables), pieces_attendues (avec leur moment), informations_requises, questions_suggerees
PorteAttendue le type de porte (contrat <domaine>.<objet>[.<sous-type>].v<n>), son audience (client · opérateur) et ce qu'elle engage (réversible · irréversible · argent)
Contours ce que le commanditaire lit avant d'envoyer : livrable, contrôle, mode, exemple, délai

Plus la nature acces : un droit d'usage récurrent (siège, abonnement, ouverture d'une application à un locataire). Ce qui le distingue d'une prestation n'est pas le prix mais l'objet — la prestation produit un livrable et s'achève, l'accès dure.

Tout y est optionnel. Une app de commerce pur n'en pose rien et ne voit aucune différence : le contrat de la 0.2.0 est intact (test de non-régression dédié).

⚠ La brique ne connaît AUCUN vocabulaire de domaine. domaine est une chaîne libre — soft, meca, cuisine, ce que l'app voudra. Le vocabulaire fermé appartient au registre du consommateur, jamais à une brique partagée : un canon gravé ici redeviendrait le canon de tout le monde. C'est exactement l'erreur qu'ADR-074 vient de fermer côté entités, et elle ne sera pas refaite ici.

from catalog_core import Offre, Fabrication, PorteAttendue, Audience, Engagement, Rail

offre = Offre(
    id="carte-2-couches", entite_id="…", societe_id="…", marque_id="…",
    nature="prestation", libelle="Carte électronique 2 couches", domaine="elec",
    fabrication=Fabrication(rail=Rail.MULTI_AGENTS, graphe="eda_lead",
                            harnais_defaut="longe-cc", harnais_autorises=["longe-cc", "claude"]),
    portes=[PorteAttendue(type="eda.fab.v1", audience=Audience.CLIENT,
                          engage=Engagement.ARGENT)],
    offres_liees=["schema-verifie"],
)
offre.porte("eda.fab.v1").engage      # <Engagement.ARGENT> — à dire AVANT qu'on tranche

Migration d'un store déjà en service : le DDL porte les ALTER TABLE … ADD COLUMN IF NOT EXISTS correspondants. CREATE TABLE IF NOT EXISTS seul ne rattrape pas une table existante — il ne fait rien, en silence.

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 produit CDC-4 rendu disponible à la vente (storefront/PIM) : produit_ref requis (R4), peut porter des claims C3.
  • prestation — un service offert (ex. le catalogue de services de la Factory) : pas de produit_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

Requirements

Requires Python: >=3.11
Details
PyPI
2026-09-12 12:38:48 +00:00
5
Holding MAPFARM
Proprietary
48 KiB
Assets (2)
Versions (5) View all
0.5.0 2026-09-20
0.4.0 2026-09-12
0.3.0 2026-09-12
0.2.0 2026-06-21
0.1.0 2026-06-21