Aller au contenu

Paramètres contextuels et résolution multi-facteurs

Version 1, 29 septembre 2026. Décisions PC-01 à PC-08 validées le 30/09/2026. Complète les pages « Paquet de règles packages/domain » et « Schéma de la base — socle ». Précise les décisions « Paquets filières » et « Paramétrage » du découpage, ainsi que EC-004, EC-007 et EC-011.

  • Une formule est du code, écrit une fois dans packages/domain. Ses valeurs (seuils, références, tolérances) sont des données, dans les packs et les paramètres.
  • Une valeur n’est pas un simple chiffre, mais une liste de règles avec conditions : « pour l’ananas séché, au Togo, en culture irriguée : 22 % ».
  • Avant chaque calcul, une fonction partagée résout la valeur applicable au cas traité. La règle la plus précise l’emporte.
  • Les ambiguïtés sont refusées à la publication du pack ou du paramètre, jamais découvertes sur le terrain.
  • Chaque opération enregistre la règle appliquée, la version et les facteurs utilisés. Un auditeur peut toujours savoir pourquoi un seuil valait ce qu’il valait.
Terme Définition Exemple
Clé de paramètre Nom d’une valeur utilisée par une formule rendement.reference
Facteur Critère dont la valeur peut changer un seuil produit, pays, type_sol
Contexte Valeurs des facteurs pour le cas traité { produit: "ananas", pays: "TG", … }
Règle Une condition sur des facteurs et la valeur qui s’applique R2 : ananas + séchage + TG + irrigué → 2 200 cp
Précision Nombre de facteurs dans la condition d’une règle R2 : précision 4
Couche Origine d’un jeu de règles : pack BioTrace ou organisation –
Résolution Choix de la règle applicable à un contexte R2 pour un ananas irrigué du Togo

Un facteur ne peut servir de condition que s’il existe comme donnée saisie, avec une liste de valeurs fermée. Un facteur qui n’est pas saisi ne peut pas être utilisé.

Facteur Porté par Valeurs Saisi au
produit Lot, collecte, contrat Produits de l’organisation (Lot 1) Lot 1
operation Transformation Types d’opération du pack (sechage, extraction_huile, pressage…) Lot 2
pays Organisation, parcelle ISO 3166-1 alpha-2 Lot 0 (organisation), Lot 1 (parcelle)
region Parcelle Liste par pays Lot 1
mode_culture Parcelle, campagne pluvial, irrigue Lot 1
type_sol Parcelle Liste du pack (lateritique, sableux, argileux…) Lot 1
referentiel Contrôle de qualité UE, NOP… Lot 1
classe_altitude Parcelle (calculée) Classes définies par le pack Lot 3

La liste est fermée. Ajouter un facteur demande :

  1. une colonne sur l’entité qui le porte ;
  2. une entrée dans le catalogue ;
  3. une nouvelle version du schéma des packs.

Pas de condition sur une plage de valeurs en v1 (« altitude entre 500 et 800 m »). Un facteur numérique est découpé en classes, définies par le pack, et une fonction de packages/domain calcule la classe à partir de la valeur mesurée :

"classes": {
"classe_altitude": {
"unite": "m",
"classes": [
{ "code": "basse", "min": 0, "max": 500 },
{ "code": "moyenne", "min": 500, "max": 1000 },
{ "code": "haute", "min": 1000, "max": null }
]
}
}

L’unité est portée une fois par facteur ; les bornes s’appellent min et max pour que le code reste indépendant de l’unité. Schéma : DefinitionsClasses dans packages/schemas. Fonctions : validerClasses (publication) et classe (résolution) dans packages/domain.

Bornes : minimum inclus, maximum exclu. Les classes doivent se suivre sans trou ni chevauchement, ce qui est vérifié à la publication.

Ce découpage garde la résolution simple, identique sur le téléphone et le serveur, et permet de vérifier l’absence d’ambiguïté.

Une région appartient à un pays, mais la résolution ne l’infère pas. Une condition sur region doit aussi porter pays, ce qui est vérifié à la publication. On évite ainsi les règles implicites.

{
"cle": "rendement.reference",
"unite": "cp",
"surchargeable": true,
"bornes_surcharge": { "min": 500, "max": 9500 },
"regles": [
{ "id": "R0", "quand": {}, "valeur": null },
{ "id": "R1", "quand": { "produit": ["ananas"], "operation": ["sechage"] }, "valeur": 2000 },
{ "id": "R2", "quand": { "produit": ["ananas"], "operation": ["sechage"],
"pays": ["TG"], "mode_culture": ["irrigue"] }, "valeur": 2200 },
{ "id": "R3", "quand": { "produit": ["avocat"], "operation": ["extraction_huile"] }, "valeur": 1000 },
{ "id": "R4", "quand": { "produit": ["avocat"], "operation": ["extraction_huile"],
"type_sol": ["lateritique"] }, "valeur": 900 },
{ "id": "R5", "quand": { "produit": ["mangue", "papaye"], "operation": ["sechage"] }, "valeur": 1500 }
]
}
  • quand associe à chaque facteur une liste de valeurs acceptées. La condition est satisfaite si le contexte a l’une d’elles, pour chaque facteur cité.
  • Une condition vide ({}) est la règle par défaut. Elle est facultative.
  • Une valeur null signifie « pas de valeur » : la résolution renvoie parametre_introuvable. C’est une façon explicite de dire « il n’y a pas de référence pour ce cas ».
  • surchargeable et bornes_surcharge limitent ce qu’une organisation peut modifier (section 5).
  • id est unique dans la clé et stable d’une version à l’autre : il sert à la traçabilité.
  1. Filtrer : garder les règles dont toutes les conditions sont satisfaites. Si un facteur de la condition est absent du contexte, la règle ne s’applique pas.
  2. Choisir : garder la règle de plus grande précision. La validation (section 6) garantit qu’elle est unique.
  3. Rendre : la valeur, l’id de la règle, la couche et la version.
  4. Aucune règle, ou valeur null : erreur parametre_introuvable. La fonction n’invente jamais de valeur.

Exemples avec le jeu ci-dessus :

Contexte Règles satisfaites Résultat
ananas, séchage, TG, irrigué R0, R1, R2 R2 → 2 200 cp
ananas, séchage, BJ, pluvial R0, R1 R1 → 2 000 cp
avocat, extraction d’huile, sol latéritique R0, R3, R4 R4 → 900 cp
papaye, séchage R0, R5 R5 → 1 500 cp
ananas, pressage R0 R0 → parametre_introuvable
Couche Source Qui la modifie
1. Pack app.pack_version, dossier packs/ du dépôt Équipe BioTrace, par publication d’une nouvelle version
2. Organisation app.parametre (ajout seul, versionné) Administrateur client, dans le back-office

La campagne n’est pas une couche. Elle fige la version du pack et la date d’application des paramètres de l’organisation.

  1. On résout d’abord dans la couche organisation.
  2. Si une règle de l’organisation s’applique, elle l’emporte, quelle que soit la précision des règles du pack.
  3. Sinon, on résout dans la couche pack.

Pourquoi la couche d’abord. Une surcharge de l’organisation exprime une réalité locale, connue du client. La règle est simple à expliquer : « si votre organisation a défini une valeur pour ce cas, c’est elle qui compte ».

Contrepartie. Une surcharge générale de l’organisation (« ananas : 18 % ») masque les règles plus précises du pack (R2). À la publication d’une surcharge, le back-office affiche la liste des règles du pack qu’elle masque, et demande une confirmation.

  • Une clé surchargeable: false ne peut pas être modifiée par l’organisation. C’est le cas des seuils réglementaires et des tolérances de bilan de masse.
  • Une surcharge doit rester dans bornes_surcharge.
  • Toute surcharge exige un motif écrit, comme tout paramètre.
  • Une référence de rendement surchargée par l’organisation est considérée comme non calibrée (EC-007) tant que l’équipe BioTrace ne l’a pas validée. Hors tolérance, elle produit donc une alerte, pas un blocage.

Un jeu de règles est refusé à la publication, pack ou surcharge, s’il présente l’une de ces anomalies.

Contrôle Refus si…
Facteurs connus Un facteur n’est pas dans le catalogue
Valeurs connues Une valeur n’est pas dans la liste du facteur
Région sans pays Une condition porte region sans pays
Identifiants Deux règles ont le même id, ou un id d’une version précédente change de sens sans changer d’identifiant
Unité et bornes Une valeur n’est pas un entier de l’unité déclarée, ou sort des bornes
Classes Classes d’un facteur numérique avec trou ou chevauchement
Ambiguïté Voir ci-dessous
Couverture Une règle n’est gagnante dans aucun vecteur de tests (section 8)

Deux règles de même précision peuvent s’appliquer au même contexte si, pour chaque facteur qu’elles citent toutes les deux, leurs listes de valeurs ont au moins une valeur en commun. On dit alors qu’elles se recouvrent. Pour chaque paire de règles de même précision qui se recouvrent :

  • si elles citent exactement les mêmes facteurs : refus. Les listes doivent être disjointes ;
  • sinon : refus, sauf si le jeu contient une règle égale à leur conjonction, c’est-à-dire l’union de leurs facteurs, avec l’intersection des valeurs pour les facteurs communs.

La conjonction s’applique à tous les contextes où les deux règles s’appliquent, et elle est plus précise. Elle tranche donc toujours. Elle est elle-même soumise au même contrôle.

Exemple :

Règle Condition Précision
A ananas + TG 2
B ananas + irrigué 2

A et B se recouvrent pour un ananas irrigué du Togo. Le pack n’est accepté que s’il contient aussi C : ananas + TG + irrigué (précision 3).

Ce contrôle est fait par validerJeuDeRegles (section 7), dans le pipeline de publication des packs et dans l’API pour les surcharges. Pour une surcharge, l’appelant reporte bornes_surcharge du pack dans le jeu à valider ; le refus d’une surcharge sur une clé surchargeable: false reste à écrire avec la route de surcharge (Lot 1).

Nouveau module parametres :

packages/domain/src/parametres/index.ts
export type CodeFacteur = string; // entrée du catalogue
export type Contexte = Readonly<Record<CodeFacteur, string>>;
export interface Regle<V> {
readonly id: string;
readonly quand: Readonly<Record<CodeFacteur, readonly string[]>>;
readonly valeur: V | null;
}
export interface JeuDeRegles<V> {
readonly cle: string;
readonly unite: string;
readonly surchargeable: boolean;
readonly bornes_surcharge?: { readonly min: number; readonly max: number };
readonly regles: readonly Regle<V>[];
}
export interface Couche<V> {
readonly origine: 'organisation' | 'pack';
readonly version: string; // version du pack ou du paramètre
readonly jeu: JeuDeRegles<V>;
}
export interface Resolution<V> {
readonly cle: string;
readonly valeur: V;
readonly regle_id: string;
readonly origine: 'organisation' | 'pack';
readonly version: string;
readonly facteurs_utilises: Contexte; // seulement les facteurs de la règle retenue
}
export function resoudre<V>(
couches: readonly Couche<V>[], // dans l'ordre de priorité
contexte: Contexte,
): Resultat<Resolution<V>, 'parametre_introuvable'>;
export function validerJeuDeRegles<V>(
jeu: JeuDeRegles<V>,
catalogue: CatalogueFacteurs,
precedent?: JeuDeRegles<V>,
): Resultat<void, 'facteur_inconnu' | 'valeur_inconnue' | 'region_sans_pays'
| 'identifiant_duplique' | 'identifiant_redefini'
| 'valeur_hors_bornes' | 'ambiguite'>;
export function reglesMasquees<V>(surcharge: JeuDeRegles<V>, pack: JeuDeRegles<V>): readonly string[];
export function classe(facteur: CodeFacteur, valeur: number, classes: DefinitionClasses): Resultat<string, 'hors_classes'>;

Les fonctions de calcul ne changent pas. Elles continuent de recevoir leurs valeurs en arguments. L’appelant résout d’abord, puis calcule :

const ref = resoudre(couches.rendementReference, contexte);
if (!ref.ok) return ref; // parametre_introuvable
const tol = resoudre(couches.rendementTolerance, contexte);
if (!tol.ok) return tol;
return evaluerRendement(entree, sortie, {
reference_cp: cp(ref.valeur.valeur),
tolerance_relative_cp: cp(tol.valeur.valeur),
calibre: ref.valeur.origine === 'pack' && packCalibre,
});

Chaque pack est livré avec ses vecteurs de résolution, dans testdata/packs/{pack}/resolution/.

{
"id": "VT-FRUITS-REND-002",
"description": "Ananas séché irrigué au Togo : règle spécifique",
"cle": "rendement.reference",
"contexte": { "produit": "ananas", "operation": "sechage", "pays": "TG", "mode_culture": "irrigue" },
"attendu": { "ok": true, "regle_id": "R2", "valeur": 2200 }
}

Règles de couverture, vérifiées à la publication :

  • chaque règle est gagnante dans au moins un vecteur ;
  • chaque paire de règles qui se recouvrent a un vecteur sur leur zone commune ;
  • si la clé n’a pas de règle par défaut, au moins un vecteur attend parametre_introuvable.

Les vecteurs de calcul existants peuvent aussi partir d’un contexte au lieu d’une valeur écrite en dur. L’exécuteur résout alors avant de calculer, ce qui teste la chaîne complète.

Chaque opération qui utilise une valeur résolue enregistre :

Donnée Où
Clé, règle retenue, origine, version Colonne resolutions (jsonb) de l’entité (transformation, collecte…)
Facteurs utilisés Même colonne : instantané des valeurs, pas une référence à la parcelle
Valeur Colonne typée de l’entité (rendement_reference_cp…)

Pourquoi un instantané. Si le type de sol d’une parcelle est corrigé plus tard, le calcul d’origine reste explicable et rejouable à l’identique.

Sur le terrain, la commande porte les résolutions faites par le téléphone (payload.resolutions). Au rejeu, le serveur résout à nouveau avec la version figée par la campagne. Une différence de règle ou de valeur ouvre un contrôle ecart_calcul (protocole, étape 11).

Où Comportement
Terrain (hors-ligne) L’opération est enregistrée. Le serveur ouvre un contrôle parametre_introuvable
Back-office Opération bloquée, avec le message : « Aucune référence de rendement pour l’ananas en pressage. Demandez à l’administrateur de la définir. »
Publication d’une campagne Avertissement listant les combinaisons produit / opération de la campagne sans valeur résolue
id Décision Statut
PC-01 Formules dans le code, valeurs dans des jeux de règles avec conditions Validé le 30/09/2026
PC-02 Catalogue de facteurs fermé, chaque facteur est une donnée saisie Validé le 30/09/2026
PC-03 Facteurs numériques découpés en classes, pas de plages dans les conditions Validé le 30/09/2026
PC-04 La règle la plus précise l’emporte à l’intérieur d’une couche Validé le 30/09/2026
PC-05 La couche organisation l’emporte sur la couche pack, avec affichage des règles masquées Validé le 30/09/2026
PC-06 Refus des ambiguïtés à la publication, par la règle de conjonction. Étendu le 02/10/2026 aux lignes des tables de décision : une ligne sans source, sans issue, sans date d’effet ou mal typée est refusée (regles.validerLigne, L0-4) Validé le 30/09/2026
PC-07 Aucune valeur inventée : parametre_introuvable, puis contrôle ou blocage Validé le 30/09/2026
PC-08 Instantané des facteurs et de la règle appliquée sur chaque opération. Étendu le 02/10/2026 aux règles évaluées : chaque constat recopie la règle, sa version, sa source et les valeurs lues (AM-15, L0-4) Validé le 30/09/2026
Document Modification
Paquet packages/domain Module parametres : fait. Exécuteur capable de résoudre avant de calculer : à faire
Schéma de la base Tables globales app.facteur et app.facteur_valeur, chargées avec les packs ; au Lot 1, colonnes region, mode_culture, type_sol sur la parcelle, avec clé étrangère vers facteur_valeur ; colonne resolutions sur les entités calculées ; motif parametre_introuvable dans sync.controle
Protocole de synchronisation payload.resolutions dans les commandes qui utilisent une valeur résolue ; motif parametre_introuvable (fait : migration 20260930130000)
Découpage Décisions PC-01 à PC-08 à reporter dans les décisions « Paquets filières » et « Paramétrage »
JSON Schema des packs Format des jeux de règles et des classes (sections 2.3 et 3) : fait, JeuDeRegles et DefinitionsClasses dans packages/schemas