Paquet de règles packages/domain
Version 2, 29 septembre 2026 : implémentation module par module (voir §6). Livrable n° 3 des travaux de conception du Lot 0. Applique les décisions « Paquet de règles », « Calculs en entiers » et « Vecteurs de tests » du découpage, ainsi que EC-004, EC-009, EC-010, EC-011, EC-013, EC-015, EC-016, EC-028 et GPS-01 à GPS-04.
packages/domaincontient toutes les règles qui doivent donner le même résultat sur le téléphone et sur le serveur.- Fonctions pures : aucune entrée-sortie, aucune lecture d’horloge, aucun hasard. Date, paramètres et données sont passés en arguments.
- Calculs en entiers, avec des types qui portent l’unité. Une addition de grammes et de centièmes de point ne compile pas.
- Les erreurs métier sont des valeurs de retour, pas des exceptions.
- Chaque fonction est livrée avec des vecteurs de tests dans
testdata/, exécutés sous Node et dans Chromium, pour couvrir le serveur et la WebView Android.
1. Règles du paquet
Section intitulée « 1. Règles du paquet »| Règle | Mise en œuvre |
|---|---|
| Pas d’entrée-sortie | Règle ESLint no-restricted-globals et no-restricted-imports : Date, Math.random, fetch, console, crypto, process et tout module Node interdits |
| Dépendances | Uniquement @noble/hashes (SHA-256) et @marcbachmann/cel-js (CEL, ADR 0043). Chacune fait l’objet d’une ADR |
| Entiers | Tout nombre d’entrée est vérifié par Number.isSafeInteger. Les divisions passent par divArrondi avec un mode explicite |
| Erreurs | Resultat<T, C> pour les erreurs métier. RangeError seulement pour une erreur de programmation (valeur non entière, unité incohérente) |
| Versions | Chaque fonction qui dépend d’un paramètre reçoit la version du paramètre en argument, jamais une valeur par défaut cachée |
| Export | Un point d’entrée par module (@biotrace/domain/pesee…), pour que le terrain ne charge que ce qu’il utilise |
2. Types de base
Section intitulée « 2. Types de base »declare const unite: unique symbol;export type Entier<U extends string> = number & { readonly [unite]: U };
export type Grammes = Entier<'g'>;export type MetresCarres = Entier<'m2'>;export type Metres = Entier<'m'>;export type Centimetres = Entier<'cm'>;export type CentiemesPoint = Entier<'cp'>; // 1 % = 100 ; 70 % = 7000export type GrammesParHectare = Entier<'g/ha'>;export type DegresE7 = Entier<'deg_e7'>; // degrés × 10^7 (WGS 84)export type Millisecondes = Entier<'ms'>;export type EpochMs = Entier<'epoch_ms'>;export type UniteMineure = Entier<'unite_mineure'>;
export type CodeDevise = string; // ISO 4217, vérifié par ^[A-Z]{3}$export interface Montant { readonly valeur: UniteMineure; readonly devise: CodeDevise }
export function g(n: number): Grammes; // lève RangeError si non entierexport function cp(n: number): CentiemesPoint;// … un constructeur par unité
export type ModeArrondi = 'plus_proche' | 'inferieur' | 'superieur'; // plus_proche : demi vers le haut
export interface Alerte { readonly code: string; readonly details?: Readonly<Record<string, unknown>> }
export type Resultat<T, C extends string = string> = | { readonly ok: true; readonly valeur: T; readonly alertes: readonly Alerte[] } | { readonly ok: false; readonly code: C; readonly details?: Readonly<Record<string, unknown>> };3. Modules
Section intitulée « 3. Modules »3.1 entiers — arithmétique de base
Section intitulée « 3.1 entiers — arithmétique de base »export function divArrondi(numerateur: number, denominateur: number, mode: ModeArrondi): number;export function mulDivArrondi(a: number, b: number, c: number, mode: ModeArrondi): number; // a × b / c, un seul arrondiexport function proportion(part: Grammes, total: Grammes, mode?: ModeArrondi): CentiemesPoint;export function appliquerTaux(quantite: Grammes, taux: CentiemesPoint, mode: ModeArrondi): Grammes;export function convertirMontant(m: Montant, taux: TauxChange, mode: ModeArrondi): Montant;
export interface TauxChange { readonly source: CodeDevise; readonly cible: CodeDevise; readonly numerateur: number; readonly denominateur: number; // EC-028 : rapport d'entiers}Les produits intermédiaires sont exacts (BigInt) ; un résultat hors des entiers sûrs (2⁵³) lève RangeError au lieu de perdre en précision. mulDivArrondi sert aux formules qui enchaînent plusieurs pourcentages : un seul arrondi final, plutôt qu’un arrondi à chaque étape.
Taux de change et unités mineures. convertirMontant applique le taux directement à la valeur du montant. La table app.taux_change stocke un taux entre devises (655 957 / 1 000 pour EUR vers XOF), alors que les montants sont en unités mineures (centimes d’euro, francs). L’appelant fournit un taux entre unités mineures (655 957 / 100 000 pour EUR vers XOF). La conversion à partir de la table est à préciser : écart EC-039.
3.2 pesee — réception et poids net marchand (EC-016)
Section intitulée « 3.2 pesee — réception et poids net marchand (EC-016) »export interface EntreePesee { readonly pesees: readonly { brut_g: Grammes; tare_g: Grammes }[]; readonly humidite_cp: CentiemesPoint | null; readonly impuretes_cp: CentiemesPoint | null;}
export type FormuleRefaction = | { readonly type: 'aucune' } | { readonly type: 'excedent_lineaire'; readonly humidite_max_cp: CentiemesPoint; readonly impuretes_max_cp: CentiemesPoint; readonly arrondi: ModeArrondi; } | { readonly type: 'lineaire_simple'; readonly humidite_norme_cp: CentiemesPoint; readonly arrondi: ModeArrondi } // formule A de RG-171 | { readonly type: 'bilan_de_masse'; readonly humidite_norme_cp: CentiemesPoint; readonly arrondi: ModeArrondi }; // formule B de RG-171
export interface DetailPesee { readonly brut_g: Grammes; readonly tare_g: Grammes; readonly net_g: Grammes; readonly excedent_cp: CentiemesPoint; readonly refaction_g: Grammes; readonly net_marchand_g: Grammes;}
export function calculerPesee( entree: EntreePesee, formule: FormuleRefaction,): Resultat<DetailPesee, 'tare_superieure_au_brut' | 'mesure_qualite_manquante' | 'aucune_pesee'>;Formule excedent_lineaire :
- net = somme des bruts − somme des tares ;
- excédent = max(0, humidité − maximum) + max(0, impuretés − maximum) ;
- réfaction = net × excédent / 10 000, arrondie selon le contrat ;
- poids net marchand = net − réfaction.
Formules A et B de RG-171 (écart EC-037, proposées) :
- base : poids net (brut − tare) ;
- A,
lineaire_simple: réfaction = net × (1 − (1 − excédent d’humidité) × (1 − impuretés)), avec excédent d’humidité = max(0, H − H norme) ; - B,
bilan_de_masse: réfaction = net × (1 − (100 − H) / (100 − H norme) × (1 − impuretés)), avec H ramenée à la norme quand elle lui est inférieure (pas de bonus) ; - un seul arrondi, sur la réfaction, selon le mode du contrat ; le poids net marchand est net − réfaction ;
- vérification : 1 000 kg à 15 %, norme 13 %, impuretés 3 % donnent 950 600 g (A) et 947 701 g (B) ;
- un pourcentage hors de 0 à 100 % est une erreur de programmation (
RangeError) : l’écran valide la saisie avant l’appel.
La formule de réfaction est portée par le contrat (Lot 1). D’autres types de formule pourront être ajoutés en CEL. Le mode d’arrondi par défaut reste à confirmer avec un contrat type.
3.3 rendement — contrôle de transformation (EC-004, EC-007, EC-011)
Section intitulée « 3.3 rendement — contrôle de transformation (EC-004, EC-007, EC-011) »export function rendementReel(entree: Grammes, sortie: Grammes): CentiemesPoint;export function ecartRendement(reel: CentiemesPoint, reference: CentiemesPoint): CentiemesPoint; // en pointsexport function ecartRelatif(reel: CentiemesPoint, reference: CentiemesPoint): CentiemesPoint; // en % de la référence
export interface ReferenceRendement { readonly reference_cp: CentiemesPoint; readonly tolerance_relative_cp: CentiemesPoint; // EC-011, portée par le pack readonly calibre: boolean; // EC-007}
export type EvaluationRendement = 'conforme' | 'alerte' | 'bloquant';
export function evaluerRendement( entree: Grammes, sortie: Grammes, ref: ReferenceRendement,): Resultat<{ reel_cp: CentiemesPoint; ecart_points_cp: CentiemesPoint; ecart_relatif_cp: CentiemesPoint; evaluation: EvaluationRendement }>;Hors tolérance : bloquant si la référence est calibrée, alerte (avec l’alerte rendement_non_calibre) sinon. La tolérance est symétrique (un rendement trop bon est aussi hors tolérance) et inclusive (l’écart égal à la tolérance est conforme). Sans écart hors tolérance, aucune alerte, même si la référence n’est pas calibrée.
Codes d’erreur ajoutés à la signature : entree_nulle et sortie_superieure_a_l_entree (RG-050), rendus comme valeurs. Une référence nulle dans ecartRelatif est une erreur de programmation.
3.4 bilan — partie double et bilan de masse (EC-015)
Section intitulée « 3.4 bilan — partie double et bilan de masse (EC-015) »export interface LigneMouvement { readonly compte: string; // identifiant du compte de matière readonly produit: string; readonly quantite_g: Grammes; // signée}
export function verifierEquilibre(lignes: readonly LigneMouvement[]): Resultat<void, 'desequilibre' | 'moins_de_deux_lignes'>;export function contrePasser(lignes: readonly LigneMouvement[]): readonly LigneMouvement[];
export interface EntreeBilanTransformation { readonly entrees_matiere_g: Grammes; readonly intrants_g: Grammes; readonly sorties_g: Grammes; // produits et coproduits readonly pertes_declarees_g: Grammes; // pertes, purges, eau évaporée readonly tolerance_relative_cp: CentiemesPoint;}
export function controlerBilanTransformation( e: EntreeBilanTransformation,): Resultat<{ ecart_inexplique_g: Grammes; ecart_relatif_cp: CentiemesPoint }, 'sortie_superieure_aux_entrees'>;calculerPertes(entree, sortie)remplace l’ancienne fonctioncalculerPertesEnGrammes(RG-050, RG-051) : la sortie supérieure à l’entrée est une valeur de retour (sortie_superieure_a_l_entree), pas une exception.- Une ligne de mouvement à zéro (interdite par la base) et un poids négatif sont des erreurs de programmation (
RangeError). - Erreur si sorties > entrées matière + intrants.
- Écart inexpliqué = entrées + intrants − sorties − pertes déclarées, signé : il est négatif quand les sorties et les pertes déclarées dépassent les entrées. L’écart relatif est rapporté aux entrées et intrants. Hors tolérance (dans les deux sens, tolérance incluse) : alerte
ecart_bilan. Des entrées nulles sont une erreur de programmation.
Ces fonctions reproduisent les contrôles de la base (déclencheur d’équilibre). Elles servent à prévenir l’utilisateur avant l’envoi, pas à remplacer la base.
3.5 capacite — plafond de collecte (EC-012, EC-013)
Section intitulée « 3.5 capacite — plafond de collecte (EC-012, EC-013) »export interface SoldeCapacite { readonly capacite_g: Grammes; // capacité validée pour la campagne readonly consomme_g: Grammes;}
export type NiveauCapacite = 'normal' | 'seuil' | 'depassement';
export function evaluerCollecte( solde: SoldeCapacite, quantite: Grammes, seuil_alerte_cp: CentiemesPoint, // 8000 par défaut (jauge du design system)): { niveau: NiveauCapacite; reste_apres_g: Grammes; excedent_g: Grammes; taux_apres_cp: CentiemesPoint };La fonction ne bloque rien. Le téléphone affiche l’avertissement, le serveur ouvre le contrôle capacite_depassee.
depassement: consommé + collecte supérieur à la capacité.seuil: au moins le seuil d’alerte ; le seuil lui-même et la capacité exacte sont inclus. Les comparaisons sont faites en entiers, sans passer par le taux arrondi.reste_apres_getexcedent_gne sont jamais négatifs et ne sont jamais tous deux non nuls. Si la capacité était déjà dépassée, tout l’excédent est signalé (RG-022).- Le seuil d’alerte n’a pas de valeur par défaut cachée (règle des versions du §1) : l’appelant le lit dans les paramètres. Une capacité nulle ou négative est une erreur de programmation ; une parcelle sans capacité validée est traitée avant l’appel.
3.6 qualite — référentiels et mélange (états par référentiel depuis L1-1+2, AM-16)
Section intitulée « 3.6 qualite — référentiels et mélange (états par référentiel depuis L1-1+2, AM-16) »export type CategorieEtat = 'bio' | 'conversion' | 'conventionnel' | 'hors_statut';export interface EtatReferentiel { code; libelle: { fr; en }; categorie: CategorieEtat; rang?: number; revendicable: boolean; vendable_sous?: string; exige_debut_conversion: boolean }export type QualiteLot = string; // code d'un état du référentielexport type QualiteParReferentiel = Readonly<Record<string, QualiteLot>>; // { UE: 'bio', NOP: 'conventionnel' }
export function validerEtats(etats: readonly EtatReferentiel[]): Resultat<null, RefusEtats>;export function intersectionQualites(lots: readonly QualiteParReferentiel[], etats: EtatsParReferentiel): QualiteParReferentiel;export function vendableSous(qualite: QualiteLot, etats: readonly EtatReferentiel[]): QualiteLot; // conversion_annee_1 → conventionnel (UE)Les états et leur ordre viennent du pack référentiel. Pour chaque référentiel, le mélange prend la qualité de plus bas rang. Un référentiel absent d’un des lots est absent du résultat ; sans lot, le résultat est vide. Une qualité inconnue, un état « hors statut » ou un référentiel sans états sont des erreurs de programmation (RangeError). L’opération est commutative, associative et idempotente (testée sur toutes les combinaisons des états de l’UE). validerEtats refuse, à la publication : liste vide, code ou rang en double, rang manquant ou posé sur un état hors statut, « vendable sous » inconnu, état conventionnel ou hors statut revendicable.
certification.peutRevendiquer({ etat, etats, certificats, date }) lit les états du référentiel : état inconnu, statut_inconnu ; hors statut, statut_hors_qualite ; non revendicable, etat_non_revendicable ; puis les raisons de certificat, inchangées. Le verdict porte l’état et l’état sous lequel l’unité reste vendable. etatVersQualite a disparu : un état est sa propre qualité.
3.7 numerotation — références provisoires et numéros officiels (EC-009, EC-010, EC-023)
Section intitulée « 3.7 numerotation — références provisoires et numéros officiels (EC-009, EC-010, EC-023) »export function referenceProvisoire(p: { code_appareil: string; // ^[A-Z0-9]{4,6}$ date_locale: string; // AAAAMMJJ, dans le fuseau de l'organisation sequence: number; // ≥ 1}): string; // BR-K7Q2-20261014-0042
export type Segment = | { type: 'annee_campagne' } // deux derniers chiffres de l'année d'ouverture | { type: 'code_campagne' } | { type: 'nature' } | { type: 'produit' } | { type: 'prefixe_entreprise'; actif: boolean } | { type: 'texte'; valeur: string } | { type: 'sequence'; chiffres: number };
export interface ModeleNumerotation { readonly version: number; readonly segments: readonly Segment[] }
export function numeroOfficiel( modele: ModeleNumerotation, valeurs: { annee_ouverture: number; code_campagne: string; nature: string; produit: string; prefixe_entreprise?: string; sequence: number },): Resultat<string, 'sequence_hors_capacite' | 'segment_manquant'>;Différence volontaire entre les deux numéros :
- la séquence de la référence provisoire s’élargit au-delà de 4 chiffres (
…-10000) : elle ne revient jamais à zéro ; - la séquence du numéro officiel est fixée à 4 chiffres par l’avenant : au-delà de 9 999, erreur
sequence_hors_capacite.
Précisions d’implémentation :
- Les séparateurs sont des segments
texte. Le format de l’avenant s’écritannee_campagne,code_campagne,texte "/",nature,texte "/",produit,texte "/",sequence 4. - Un
prefixe_entrepriseinactif ne produit rien, et le segmenttextequi le suit (son séparateur) est omis. Actif sans valeur :segment_manquant. Une valeur vide pour le code de campagne, la nature ou le produit :segment_manquant. - Une entrée mal formée (code d’appareil, date, séquence à zéro, modèle sans segment de séquence ou avec plusieurs) est une erreur de programmation (
RangeError). - Le module compose le numéro. La continuité de la séquence (RG-142) est garantie par la base, au Lot 1.
3.8 commande — enveloppe, empreinte et date d’opération
Section intitulée « 3.8 commande — enveloppe, empreinte et date d’opération »export function jsonCanonique(valeur: unknown): string; // RFC 8785 (JCS)export function sha256Hex(texte: string): string;export function empreinteCommande(enveloppe: Omit<EnveloppeCommande, 'empreinte'>): string;export function contenuQr(e: EnveloppeCommande): string; // référence, id, 16 premiers caractères de l'empreinte
export interface EntreeDateOperation { readonly cree_le_appareil: EpochMs; readonly horloge: { ref_serveur: EpochMs; ms_depuis_ref: Millisecondes } | null; readonly derniere_synchro: EpochMs | null; readonly recue_le: EpochMs; readonly ecart_tolere_ms: Millisecondes;}
export function dateOperation( e: EntreeDateOperation,): Resultat<{ date_ms: EpochMs; source: 'appareil' | 'estimee' | 'reception' }, never>;// alertes possibles : horloge_corrigee, horloge_invraisemblableEnveloppeCommande est le type généré depuis le JSON Schema de l’enveloppe (packages/schemas), pas une définition manuelle. Ce paquet n’existant pas encore, empreinteCommande accepte un objet quelconque (Enveloppe) et ignore son champ empreinte ; le type généré la remplacera.
Précisions d’implémentation :
jsonCanoniquesuit la RFC 8785 : clés triées par unités de code UTF-16, nombres à la manière d’ECMAScript (l’annexe B de la RFC est rejouée dans les tests). Une propriétéundefinedest omise ; NaN, infinis, bigint, fonctions et objets non simples sont des erreurs de programmation.sha256Hexutilise@noble/hashes(ADR 0033). Vecteurs du NIST.contenuQr:{référence provisoire}|{id}|{16 premiers caractères de l'empreinte}. Le format exact n’est fixé par aucune source : écart EC-040, à valider avec la page de vérification du Lot 5.dateOperation: sans dernière synchronisation, il n’y a pas de borne basse ; une date ramenée à la réception (règle 5) a pour sourcereception, sans alerte, sauf l’alertehorloge_invraisemblablede la règle 4. L’alertehorloge_corrigeeporte l’écart (ecart_ms).
3.9 gps — points, polygones et surfaces (GPS-01 à GPS-04)
Section intitulée « 3.9 gps — points, polygones et surfaces (GPS-01 à GPS-04) »export interface PointGps { readonly lat: DegresE7; readonly lon: DegresE7; readonly precision_cm: Centimetres; readonly horodatage_ms: EpochMs;}
export interface ParametresFiltrage { readonly version: number; readonly precision_max_cm: Centimetres; // 800 readonly vitesse_max_cm_s: number; // 300 readonly lectures_stabilisation: number; // 5}
export type RaisonRejet = 'precision_insuffisante' | 'saut_incoherent' | 'avant_stabilisation';
export function filtrerPoints(points: readonly PointGps[], p: ParametresFiltrage): { retenus: readonly PointGps[]; rejetes: readonly { point: PointGps; raison: RaisonRejet }[] };
export function moyenneSommet(points: readonly PointGps[]): PointGps; // mode sommetsexport function simplifier(points: readonly PointGps[], tolerance_cm: Centimetres): readonly PointGps[]; // Douglas-Peuckerexport function fermerAnneau(points: readonly PointGps[]): readonly PointGps[];
export function validerPolygone(anneau: readonly PointGps[]): Resultat<void, 'moins_de_trois_sommets' | 'auto_intersection' | 'surface_nulle'>;
export function surfaceM2(anneau: readonly PointGps[]): MetresCarres; // surface géodésiqueexport function perimetreM(anneau: readonly PointGps[]): Metres;export function incertitudeSurfaceM2(perimetre: Metres, precision_mediane_cm: Centimetres): MetresCarres;
export function evaluerEcartSurface(declaree: MetresCarres, mesuree: MetresCarres, incertitude: MetresCarres): { ecart_m2: number; ecart_relatif_cp: CentiemesPoint; anomalie: boolean };Une exception assumée à la règle des entiers. Distances et surfaces demandent de la trigonométrie. Le calcul interne se fait en flottants, puis le résultat est arrondi à l’entier. Les fonctions trigonométriques ne donnent pas toujours des résultats identiques au bit près d’un moteur JavaScript à l’autre. D’où trois règles :
- le serveur fait foi : la surface officielle est celle de
ST_Area(geography); - les vecteurs de tests comparent
surfaceM2à PostGIS avec une tolérance de 0,1 % ; - au rejeu serveur, si le filtrage retient d’autres points que le téléphone, le calcul serveur l’emporte. Au-delà de 0,5 % d’écart de surface, contrôle
ecart_calcul.
Les décisions de rejet qui ne dépendent que de la précision annoncée (comparaison d’entiers) sont, elles, strictement identiques des deux côtés.
Précisions d’implémentation :
- Projection locale. Le calcul se fait sur le plan tangent au centre du polygone, avec les rayons de courbure de l’ellipsoïde WGS 84 à cette latitude. Une sphère donnerait environ 0,45 % d’erreur à la latitude de Lomé, au-delà des 0,1 % exigés. Sur les vecteurs, l’écart avec PostGIS est inférieur à 0,02 % (rectangle de 10 km² compris), l’arrondi au mètre carré pesant plus lourd sur les très petites parcelles. Valable pour des parcelles de quelques kilomètres, hors antiméridien.
- Prédicats exacts. Orientation et intersection de côtés en
BigInt: les produits de degrés × 10⁷ dépassent 2⁵³.validerPolygonedétecte le croisement de deux côtés, un sommet répété, un sommet posé sur un côté et une pointe qui revient sur elle-même (auto_intersection, avec les deux côtés en cause) ; des sommets alignés donnentsurface_nulle. - Filtrage (
filtrerPoints) : un point de précision insuffisante est rejeté et remet à zéro la série de lectures stables tant que le démarrage n’a pas eu lieu. La lecture qui complète la série delectures_stabilisationest la première retenue ; les précédentes sontavant_stabilisation. Le saut est mesuré depuis le dernier point retenu ; deux lectures au même instant à des positions différentes forment un saut. - Moyenne d’un sommet : moyenne arrondie des positions et des heures, précision médiane des lectures (GPS-03).
- Incertitude : périmètre × précision médiane, en entiers (GPS-04). L’écart de surface est signé (mesuré − déclaré) et n’est une anomalie que s’il dépasse l’incertitude.
- Vérification contre PostGIS.
pnpm db:surfaces(scriptdb/tests/surfaces-gps.mjs, exécuté par la CI) recalculeST_AreaetST_Perimeterdes polygones des vecteurs et les compare à leur valeur attendue, à 0,1 % près.
3.10 droits — séparation des tâches
Section intitulée « 3.10 droits — séparation des tâches »export type RegleSeparation = | 'auteur_different_validateur' // EC-012 | 'validateurs_distincts' // EC-012 | 'decideur_different_auteur_commande' // file de contrôle | 'inspecteur_sans_conflit'; // AV1-07, Lot 4
export function verifierSeparation(regle: RegleSeparation, contexte: ContexteSeparation): Resultat<void, 'separation_des_taches'>;
export function permissionRequise(typeCommande: string): string; // table de correspondance figée dans le codeCes règles sont figées dans le code (découpage : « contraintes de séparation des tâches figées »). Les ensembles de permissions, eux, sont configurables par organisation.
Précisions d’implémentation :
ContexteSeparationdépend de la règle :auteur_idetvalidateur_id; la liste des validations (rôle, validateur) ;decideur_idetauteur_commande_id; pour l’inspecteur, le membre inspecté, le membre correspondant à l’inspecteur s’il est exploitant, et ses liens déclarés (famille ou exploitation). Le contexte deinspecteur_sans_conflitest provisoire : le modèle des liens déclarés arrive au Lot 4.- L’erreur
separation_des_tachesporte la règle en cause et l’utilisateur ou la nature du conflit. Une règle inconnue est une erreur de programmation. permissionRequise(EC-038, option 1 modifiée) : chaque type de commande est relié à une permission anglaise par la tablePERMISSION_PAR_TYPE(collecte.enregistrer→collection.record) ; un type hors catalogue lèveRangeError. Le catalogue (PERMISSION_PAR_TYPE,PERMISSIONS_BACKOFFICE),validerPermissions(permission_inconnue) etENSEMBLES_PAR_DEFAUT(les neuf ensembles de la page Keycloak §6, ceux des lots 2 et suivants vides) sont dansdroits/permissions.ts. Un nouveau type de commande entre d’abord au catalogue : le registre de l’API refuse d’enregistrer un gestionnaire dont le type n’y est pas. La base valide l’auteur des validations et des levées de contrôle (SB-11, SB-12).
3.11 expressions — CEL (décidé, ADR 0043)
Section intitulée « 3.11 expressions — CEL (décidé, ADR 0043) »export type TypeVariable = 'entier' | 'texte' | 'booleen' | 'liste_texte' | 'table_texte';export function verifierExpression(source: string, declarations: Declarations, fonctions?: Fonctions): Resultat<{ variables: string[]; type: TypeVariable | 'autre' }, 'expression_invalide' | 'variable_inconnue' | 'type_incompatible'>;export function evaluerExpression(source: string, variables: Readonly<Record<string, ValeurVariable>>, fonctions?: Fonctions): Resultat<ValeurVariable, 'expression_invalide' | 'variable_manquante' | 'erreur_evaluation'>;- Les variables sont nommées par leur nom pointé du dictionnaire (
etape.mesures.humidite_pm), comme dans AM-09. verifierExpressionsert à la publication (PC-06, AM-13) : toute variable lue est déclarée, y compris soushas()(P5), les types sont compatibles et aucun littéral décimal n’est admis. Elle rend le type du résultat : une condition de table de décision doit rendrebooleen.evaluerExpressionpasse les entiers enint64 bits. Une variable lue et absente rendvariable_manquanteavec le segment absent (PC-07) ; un court-circuit ouhas()ne lit pas la variable. Division par zéro et dépassement :erreur_evaluation. Variable non entière, résultat hors des entiers sûrs : RangeError.fonctionsdéclare les fonctions de l’appelant, commeparam(texte): entier(L0-4). Leurs valeurs sont résolues avant l’appel ; le domaine ne va rien chercher.- Premier appelant :
regles.evaluerPoint(Lot 0 bis, L0-4).
4. Vecteurs de tests
Section intitulée « 4. Vecteurs de tests »4.1 Format
Section intitulée « 4.1 Format »Un fichier par cas, dans testdata/domain/{module}/{fonction}/.
{ "id": "VT-PESEE-001", "description": "Réfaction sur excédent d'humidité de 2 points", "sources": ["EC-016", "RG-171"], "fonction": "pesee.calculerPesee", "entree": { "entree": { "pesees": [{ "brut_g": 105000, "tare_g": 5000 }], "humidite_cp": 1400, "impuretes_cp": 150 }, "formule": { "type": "excedent_lineaire", "humidite_max_cp": 1200, "impuretes_max_cp": 200, "arrondi": "plus_proche" } }, "attendu": { "ok": true, "valeur": { "brut_g": 105000, "tare_g": 5000, "net_g": 100000, "excedent_cp": 200, "refaction_g": 2000, "net_marchand_g": 98000 }, "alertes": [] }}attendupeut référencer une valeur d’un pack ({ "$pack": "[email protected]", "chemin": "rendements.jus.reference_cp" }) au lieu d’une valeur écrite en dur (EC-004).- Extensions du format, utilisées par l’exécuteur :
leve: "RangeError"à la place deattendupour une erreur de programmation ;comparaison: { "tolerance_relative_cp": 10 }pour comparer des nombres avec une tolérance relative (surfaces : 10 cp = 0,1 %). Les valeurs deentreesont passées dans l’ordre des paramètres de la fonction. - Un exécuteur unique lit tous les fichiers et appelle la fonction nommée. Il tourne sous Vitest en mode Node et en mode navigateur (Chromium), et dans le serveur pour les fonctions rejouées à la synchronisation.
- Les vecteurs de surface sont aussi exécutés contre PostGIS, avec la tolérance de la section 3.9.
- Toute divergence bloque l’intégration continue.
4.2 Premiers vecteurs
Section intitulée « 4.2 Premiers vecteurs »| id | Fonction | Cas | Attendu |
|---|---|---|---|
| VT-PESEE-001 | calculerPesee |
Exemple ci-dessus | Net marchand 98 000 g |
| VT-PESEE-002 | calculerPesee |
Tare > brut | tare_superieure_au_brut |
| VT-PESEE-003 | calculerPesee |
Formule avec maximum, humidité absente | mesure_qualite_manquante |
| VT-PESEE-004 à 013 | calculerPesee |
Formules A et B de RG-171, double pesée, arrondis, mesures absentes, pourcentage hors limites | Voir testdata/domain/pesee/ |
| VT-REND-001 | evaluerRendement |
SR-10 : 10 000 kg → 7 000 kg, référence du pack | Réel 7 000 cp ; écart −1 000 cp (−10 points) ; écart relatif −1 250 cp |
| VT-REND-002 | evaluerRendement |
Même écart, référence non calibrée | alerte + rendement_non_calibre |
| VT-REND-003 à 013 | rendement.* |
Tolérance (limite, sens inverse), non calibré dans la tolérance, entrée nulle, sortie supérieure, écarts | Voir testdata/domain/rendement/ |
| VT-BILAN-001 | verifierEquilibre |
Lignes +98 000 / −98 000 | Équilibré |
| VT-BILAN-002 | verifierEquilibre |
Lignes +98 000 / −97 999 | desequilibre |
| VT-BILAN-003 | controlerBilanTransformation |
100 kg + 5 kg de sucre → 70 kg de jus + 35 kg de pertes | Écart inexpliqué 0 |
| VT-BILAN-004 | controlerBilanTransformation |
Sortie 106 kg pour 100 kg + 5 kg | sortie_superieure_aux_entrees |
| VT-BILAN-005 à 017 | bilan.* |
Moins de deux lignes, séchage, contre-passation, tolérance, écart négatif, pertes | Voir testdata/domain/bilan/ |
| VT-CAPA-001 | evaluerCollecte |
Capacité 1 000 kg, consommé 700 kg, collecte 150 kg | seuil (85 %) |
| VT-CAPA-002 | evaluerCollecte |
Même solde, collecte 420 kg | depassement, excédent 120 kg |
| VT-CAPA-003 à 009 | evaluerCollecte |
Normal, seuil exact, capacité exacte, un gramme de trop, déjà dépassée, autre seuil, capacité nulle | Voir testdata/domain/capacite/ |
| VT-QUAL-001 | intersectionQualites |
UE bio + UE conversion A2 | UE conversion A2 |
| VT-QUAL-002 à 011 | qualite.* |
Plusieurs référentiels, référentiel absent, aucun lot, ordre, qualité inconnue, vendableSous |
Voir testdata/domain/qualite/ |
| VT-NUM-001 | referenceProvisoire |
K7Q2, 20261014, 42 | BR-K7Q2-20261014-0042 |
| VT-NUM-002 | referenceProvisoire |
Séquence 10 000 | BR-K7Q2-20261014-10000 |
| VT-NUM-003 | numeroOfficiel |
Format de l’avenant, campagne 2026-2027 code A, collecte, ananas, 137 | 26A/COL/ANA/0137 |
| VT-NUM-004 | numeroOfficiel |
Séquence 10 000 | sequence_hors_capacite |
| VT-NUM-005 à 016 | numerotation.* |
Dernière séquence, deux campagnes, préfixe actif ou non, segment manquant, entrées mal formées | Voir testdata/domain/numerotation/ |
| VT-CMD-001 | jsonCanonique |
Vecteurs de la RFC 8785 | Sortie identique à la RFC |
| VT-CMD-002 | dateOperation |
Écart d’horloge 3 jours, horloge renseignée | Source estimee, alerte horloge_corrigee |
| VT-CMD-003 à 027 | commande.* |
Tri RFC 8785, SHA-256 du NIST, date d’opération (tolérance, redémarrage, bornes) | Voir testdata/domain/commande/ |
| VT-GPS-001 | filtrerPoints |
Trace réelle avec 3 points à 12 m de précision | 3 rejets precision_insuffisante |
| VT-GPS-002 | validerPolygone |
Polygone en nœud papillon | auto_intersection |
| VT-GPS-003 | surfaceM2 |
Carré de 100 m de côté à la latitude de Lomé | 10 000 m² à 0,1 % près, et égal à PostGIS à 0,1 % près |
| VT-GPS-004 | incertitudeSurfaceM2 |
Périmètre 400 m, précision 300 cm | 1 200 m² (GPS-04) |
| VT-GPS-005 à 063 | gps.* |
Surfaces de six polygones comparées à PostGIS, périmètre, polygones invalides, fermeture, filtrage (stabilisation, saut), moyenne, simplification, écart de surface | Voir testdata/domain/gps/ |
Les traces des vecteurs de filtrage sont fabriquées : la trace réelle de VT-GPS-001 viendra des enregistrements du Lot 3.
Les codes de nature et de produit de VT-NUM-003 (COL, ANA) sont des exemples : les vraies valeurs viennent du paramétrage du Lot 1.
Les traces réelles du test comparatif GPS-07 viendront compléter les vecteurs gps au Lot 3.
5. Organisation du code
Section intitulée « 5. Organisation du code »Écart avec l’arborescence ci-dessous. Les règles d’interdiction de la section 1 sont dans eslint.config.js à la racine, pour packages/domain/src/** : ESLint 10 utilise la configuration la plus proche sans la cumuler avec celle de la racine, ce qui obligerait à recopier les règles communes. Un test placé dans src/ peut tout utiliser ; l’exécuteur est dans test/. Turborepo invalide le cache du test quand testdata/domain/** change (packages/domain/turbo.json).
packages/domain/ src/ types.ts entiers/ pesee/ rendement/ bilan/ capacite/ qualite/ numerotation/ commande/ gps/ droits/ expressions/ modules/ dictionnaire/ regles/ index.ts test/ executeur-vecteurs.test.ts # lit testdata/domain/** eslint.config.js # règles d'interdiction de la section 1 package.json # exports par module
testdata/ domain/{module}/{fonction}/*.json packs/{pack}/*.json # vecteurs livrés avec chaque pack traces-gps/*.geojson # traces brutes réelles6. État d’avancement
Section intitulée « 6. État d’avancement »| Module | État | Vecteurs |
|---|---|---|
types, entiers |
Fait | VT-ENT-001 à 016 |
pesee |
Fait (trois formules) | VT-PESEE-001 à 013 |
rendement |
Fait | VT-REND-001 à 013 |
bilan |
Fait (remplace calculerPertesEnGrammes) |
VT-BILAN-001 à 017 |
capacite |
Fait | VT-CAPA-001 à 009 |
qualite |
Fait ; états par référentiel et validerEtats depuis L1-1+2 (page) |
VT-QUAL-001 à 011 |
numerotation |
Fait | VT-NUM-001 à 016 |
commande |
Fait (ADR 0033) | VT-CMD-001 à 027 |
gps |
Fait | VT-GPS-001 à 063 (sauf traces réelles, Lot 3) |
droits |
Fait, catalogue des permissions compris (EC-038) | VT-DROITS-001 à 017 |
parametres |
Fait : resoudre, validerJeuDeRegles, reglesMasquees, classe, validerClasses ; décisions PC-01 à PC-08 (page dédiée) |
VT-PARA-001 à 013 |
expressions |
Fait (ADR 0043) ; appelé par regles (L0-4) |
VT-EXPR-001 à 028 |
dictionnaire |
Fait : declarations, aplatir, disponibleHorsLigne (L0-4) |
VT-DICO-001 à 004 |
regles |
Fait : evaluerPoint, issueLaPlusForte, bloqueAction, validerLigne, compilerLivret ; premier appelant d’expressions ; appelé par evaluerAccroche de l’API (L0-4) |
VT-REGLE-001 à 012 |
modules |
Fait : etatModules, verifierChangement, modulesProposes (L0-2) ; manifestes MANIFESTES, moduleDeRoute, moduleDePermission, moduleDeCommande, moduleDEcran, catalogueDepuisManifestes (L0-3) |
VT-MOD-001 à 021 |
| Exécution dans Chromium | Étape séparée : Vitest en mode navigateur, Playwright, ADR, téléchargement de Chromium à valider |