1# Famille 2 « L'argent » (constats 6 à 9) confrontée au déployé du 14/09/2026 2 3Auditeur : `audit-argent`. Régime : lecture seule stricte, aucun appel réseau, aucune clé employée. 4Corpus confronté : `code/flexylead-crm-2026-09-14/` (HEAD `ccbbaa07`) contre `code/flexylead-crm-0908/` (`730a5e79`). 5Antériorité : `analyse/preuves-argent-2026-09-09.md`, `analyse/argent-agent-administration-2026-09-13.md`. 6 7--- 8 9## 1. Périmètre traité et exclusions 10 11Traité : les quatre constats de la famille 2 ; le sort de `admin_economics_apply` ; tout changement, adaptateur ou non, situé sur un chemin qui produit un montant ou qui déclenche un palier ; les trois fonctions neuves qui écrivent ; le statut probatoire de `backend/`. 12 13Exclu : l'adaptateur d'autorisation en tant que dispositif de sécurité (périmètre `audit-agents-ia` / `audit-donnees`) — je ne le juge que sur une question, celle de savoir s'il modifie un montant. Exclu également : `leadbyte-prospect-dispatcher` et `flexylead-discard-missed-call` au-delà du contrôle « touchent-ils une table d'argent » ; la clé Composio en clair (§8, à router vers `audit-integrations`). 14 15--- 16 17## 2. Réserve liminaire : le statut de `backend/` s'est dégradé, et `backend/` n'est pas homogène 18 19### C0 · La convention de lecture a disparu, et rien ne la remplace · `FAIT` 20 21La phrase du README du 08/09 — « `backend/` contains deployed-source snapshots; these are not the editing source of truth » (`flexylead-crm-0908/README.md:14`) — **n'existe plus**. Le README est passé de 162 à 111 lignes et a été entièrement réécrit (`diff` des deux README : 1ʳᵉ ligne, titre changé de « Flexylead CRM — Unified Partners App » à « Flexylead Partenaires »). Le nouveau texte ne qualifie **jamais** `backend/`. 22 23`CANONICAL_SOURCE.json` est **inchangé** entre les deux instantanés et porte toujours `"deployment_status": "staging-candidate"`. 24 25Conformément à la consigne : **« `backend/` = le déployé » est traité ici comme une déclaration** (le message de commit), non comme un fait. 26 27### C1 · Tout le changement est dans `backend/` ; `functions/` n'a pas bougé d'un octet · `FAIT` 28 29`diff -rq flexylead-crm-0908/functions flexylead-crm-2026-09-14/functions` → **aucune sortie**. 147 fichiers de chaque côté, byte-à-byte identiques. 30 31`backend/` passe de 84 à 101 entrées. 63 des 99 `.mjs` de `backend/` portent `requireTrustedPrincipal` ; **0 des 147** fichiers de `functions/`. 32 33L'inversion signalée est confirmée : le prétendu déployé est en avance sur la source de travail, à l'inverse de la convention du 08/09. 34 35### C2 · `backend/` contient en réalité trois populations distinctes — et c'est ce qui tranche la question du MCP · `FAIT` 36 37| Population | Signature | Lecture | 38|---|---|---| 39| **(a)** 63 fichiers | portent l'adaptateur, **absent** de leur jumeau `functions/` | cohérent avec un tirage depuis une production en avance | 40| **(b)** `flexylead-admin-mcp.mjs` | **byte-à-byte identique à `flexylead-crm-0908/functions/flexylead-admin-mcp.mjs`** (`diff -q` → identiques) ; adaptateur = 0 | cohérent avec une **copie de la source de travail du 08/09**, pas avec un tirage de production | 41| **(c)** `flexylead-sync.mjs`, `process-appel.mjs`, `flexylead-mirror-specific-audios.mjs`, `flexylead-debug-list-drive-folder-reda.mjs`, 8 fichiers `tmp-*.mjs` | **absents de `functions/`**, adaptateur = 0 | fichiers ad hoc, sans jumeau source | 42 43**Conséquence directe, et elle contredit la prémisse n°1 du cadrage :** que `flexylead-admin-mcp.mjs` figure dans `backend/` **n'établit pas qu'il est déployé**. Le fichier est l'exact octet de la source de travail du 08/09 ; il n'a reçu aucune des 49 lignes d'adaptateur que ses 63 voisins ont reçues. Si `backend/` était un tirage de production, il faudrait expliquer pourquoi ce fichier-là, et lui seul parmi les fonctions d'écriture, aurait échappé au durcissement **tout en restant rigoureusement identique à la version pré-durcissement**. L'explication la plus économe est qu'il a été **copié dans l'instantané**, non tiré de la production. 44 45**Donc : ma vérification V3 n'est pas répondue par ce corpus. Elle reste ouverte.** Je maintiens le niveau `HYPOTHÈSE` sur le déploiement du MCP. 46 47### C3 · La contradiction « présent dans `backend/` mais absent du manifeste » s'explique sans supposer le déploiement · `FAIT` 48 49`flexylead-admin-mcp` → **0 occurrence** dans `scripts/application-api-manifest.mjs` et dans `scripts/deploy-local-application-api.mjs` (confirmé sur le corpus du jour). 50 51Fait neuf et décisif : les entrées du manifeste pointent vers **`../../functions/`** : 52 53``` 54{ name: 'flexylead-manage-production-economics', file: '../../functions/flexylead-manage-production-economics.mjs', … } 55``` 56(`scripts/application-api-manifest.mjs:75`, `scripts/deploy-local-application-api.mjs:89`) 57 58Or `functions/` **ne contient pas l'adaptateur**. Si ce script était le chemin de déploiement réel, l'adaptateur des 63 fichiers ne serait jamais arrivé en production. Les deux lectures possibles : 59 601. `backend/` est un vrai tirage d'une production déployée **par un autre chemin** (l'app Code d'Apteva) — alors le manifeste n'est pas l'inventaire de la production, et mon constat du 09/09 sur l'absence du MCP au manifeste perd sa portée probatoire dans les deux sens : il ne prouve ni l'absence, ni la présence. 612. `backend/` n'est pas le déployé — alors le manifeste reste l'inventaire et le constat du 09/09 tient tel quel. 62 63**Ce que les deux lectures ont en commun, et que je retiens : le manifeste livré n'est pas une source fiable de l'état de la production.** C'est le seul énoncé que le corpus autorise. 64 65--- 66 67## 3. Sort de mon constat du 13/09 sur `admin_economics_apply` : il survit, et il se durcit 68 69### C4 · Le défaut de `:546` est intact dans l'instantané du jour · `FAIT` 70 71`backend/flexylead-admin-mcp.mjs:546` : 72 73```js 74const forwardedActor = args.actor || { auth_user_id: actor.id, email: actor.email, permissions: actor.permissions }; 75``` 76 77`forwardableActor(actor)` apparaît aux lignes `:833`, `:861`, `:883`, `:896`, `:999`, `:1019`, `:1031`, `:1043`, `:1061`, `:1080`, plus sa définition `:1381` et son export de test `:1497`. **`economicsApply` reste le seul outil d'écriture à ne pas l'employer.** 78 79### C5 · L'adaptateur ne débloque pas l'apply — il ajoute un mode d'échec · `FAIT` sur le code, `SUGGÉRÉ` sur l'effet 80 81`authenticateActor` de la cible a été réécrit (`backend/flexylead-manage-production-economics.mjs:685-692`) : 82 83```js 84async function authenticateActor(context, body, event = context?.[__authParityEvent]) { 85 let trusted = null; 86 try { 87 trusted = await trustedActor(event, context, body?.actor); 88 } catch (error) { 89 if (!/Authenticated Function context required/i.test(String(error?.message || error))) throw error; 90 } 91 if (trusted) return trusted; 92``` 93 94Trois lectures, dans l'ordre : 95 961. **Le repli est préservé.** Sans contexte authentifié, `trustedActor` rend `null` (`:703`, commentaire de l'auteur : *« Existing explicitly authenticated service workflows remain separate »*), puis l'ancien chemin se déroule : `testMode`, puis l'assertion de service (`assertedActorFromServiceLink`, `:723+`), puis le jeton, puis la levée **« Session gérant non authentifiée. »** (`:704`). **Le diagnostic de C5 du 13/09 est donc inchangé : l'apply échoue toujours, dès la lecture, pour la même raison.** 972. **Un mode d'échec neuf apparaît.** Le `catch` ne rattrape **que** le message « Authenticated Function context required ». Les deux autres levées de `trustedActor` — « User is inactive or outside the authenticated organization. » et **« Command actor does not match the authenticated user. »** (`:719-722`) — **remontent**. Or `forwardedActor` transmet `auth_user_id: actor.id` et `email: actor.email`. Si la plateforme fournit un principal authentifié pour un appel fonction-à-fonction et que cette identité diffère de celle transmise, l'apply échoue désormais **plus bruyamment**, pas moins. 983. **Dans aucune des deux branches l'apply ne devient opérant.** Le durcissement ne referme pas la porte de C5 ; il ne l'ouvre pas non plus. 99 100**Réponse à « est-ce un oubli, ou ce fichier n'en a-t-il pas besoin parce qu'il est appelé en MCP et non en HTTP ? »** — les deux hypothèses sont compatibles avec l'absence d'adaptateur **dans** le MCP, car l'adaptateur protège une *entrée* HTTP et le MCP a la sienne (`resolveActor`, `:1247-1291`). Mais elles ne rendent compte ni de l'identité byte-à-byte avec la source du 08/09 (C2), ni du fait que la **cible** de l'apply, elle, a été durcie. L'hypothèse « appelé autrement, donc exempté » **n'est pas écartée**, mais elle n'explique pas l'anomalie de provenance. 101 102### C6 · V4 est répondue, et elle renforce la thèse de l'oubli · `FAIT` 103 104`FLEXYLEAD_ACTOR_ASSERTION_SECRET` est référencé par **12 fichiers**, dont `backend/flexylead-manage-production-economics.mjs:728` (côté cible) et `backend/flexylead-admin-mcp.mjs:1383` (dans `forwardableActor`, côté appelant). 105 106**La liaison de service existe donc bel et bien pour les dix autres outils d'écriture.** Le contraste de C5 est une omission isolée sur une ligne, non une architecture différente. Je fais passer « je penche pour l'oubli, sans pouvoir le prouver » (13/09) à **`SUGGÉRÉ` fortement étayé**. 107 108--- 109 110## 4. Le calcul de rémunération lui-même n'a pas bougé 111 112### C7 · Le cœur du calcul est identique, au caractère près · `FAIT` 113 114Le bloc allant de `evaluateMonthlyFormula` à `saleRemunerationForEntry` inclus : 115 116- 0908 : `backend/flexylead-app-pilotage-remuneration-query.mjs:2264-2560` 117- 0914 : `backend/flexylead-app-pilotage-remuneration-query.mjs:2360-2656` 118 119`diff` des deux extraits → **aucune différence**. `evaluateMonthlyFormula` et `saleRemunerationForEntry` n'ont pas bougé ; ils se sont seulement **décalés de 96 lignes**, exactement le volume de l'adaptateur inséré en tête. 120 121Les six fonctions de pilotage reçoivent bien **+377 / −50 chacune** (mesuré). Elles restent six fichiers distincts (six empreintes MD5 différentes). 122 123### C8 · Les défauts chiffrés du 13/09 sont tous intacts · `FAIT` 124 125`backend/flexylead-manage-production-economics.mjs`, valeurs par défaut de `normalizeMonthlyFormula` : `base_amount` **80** (`:391`), `eligibility_metric` `transformation_rate` (`:392`), seuil **12** (`:394`), `ineligible_amount` **0** (`:395`), `minimum_amount` **0** (`:384`), `maximum_amount` **100** (`:385`), `adjustments` absents → `defaultFormula()` (`:343`). 126 127`defaultFormula()` (`:413-422`) contient toujours **les cinq lignes du barème B**, au libellé près : Météore < 15 % → −5, Météore ≥ 15 % → +5, Météore ≥ 25 % → +5, ConsoPilote ≥ 15 % → +5, score IA ≥ 90 % → +5. 128 129**X1 (+22 €/contrat, 4 400 €/mois par centre) est donc inchangé sur le déployé du jour.** 130 131Hors adaptateur, le diff de ce fichier se réduit à un **changement d'encodage** (`é` → `\xE9`) et à des virgules terminales supprimées. `versionRule`, `validatedEffectiveFrom` et `ruleDateTime` sont intacts : **C9 (pas de réécriture du passé) et C13 (fenêtre UTC de deux heures) du 13/09 tiennent tels quels.** 132 133Même contrôle, même résultat pour `flexylead-manage-stats-periods.mjs` (hors adaptateur : encodage seul → **C11, « le 26 n'existe pas dans le code », tient**) et pour `flexylead-manage-offer-catalog.mjs` (hors adaptateur : encodage + suppression d'un commentaire → **constat 8 / 3.3, rétroactivité libre de la prime, tient**). 134 135`flexylead-create-sales.mjs` et `flexylead-update-sale.mjs` : hors adaptateur, un renommage `clean` → `clean2`, l'export de `findLogicalRow`, et un durcissement du test de référence numérique. **Aucune ligne de la logique `prime_montant` / `prime_rule_id`.** 136 137`config/tables/production-economics.json` : **toujours aucune colonne `created_by` / `updated_by`**. Le constat C6 du 13/09 — aucune trace d'auteur sur une modification de barème — est inchangé. 138 139--- 140 141## 5. Deux changements non signalés se trouvent pourtant sur le chemin de l'argent 142 143Le cadrage annonçait que les +377/−50 étaient « l'adaptateur ». C'est vrai pour ~49 lignes. **Le reste est autre chose, et il touche le moteur de rémunération.** Je les ai donc instruits. 144 145### C9 · Mémoïsation de la sélection des règles — vérifiée saine, mais elle n'est pas neutre à lire · `FAIT` 146 147Nouveau dispositif (`backend/flexylead-app-pilotage-remuneration-query.mjs:670-685`) : un `WeakMap` `activeRuleCache`, armé et restauré par `buildPilotageProduction` dans un `try/finally`, le calcul réel étant déplacé dans `buildPilotageProductionUncached` (`:686`). 148 149Deux fonctions d'argent passent désormais par ce cache : 150 151- `costRuleFor` (`:2214-2215`), clé `['cost', type, unit, row.date, row.commercialId, row.campaignId, row.centreId]` 152- `monthlyFormulaRuleFor` (`:2325-2326`), clé `['monthly', clean(centreId), date]` 153 154**J'ai cherché la collision de clé, c'est-à-dire le cas où le montant serait faux.** Je ne l'ai pas trouvée : 155 156- le corps de `costRuleFor` ne consomme que `cost_type`, `unit`, `effectiveOn(rule, row.date)` et `scopeMatches(rule, row)` ; `scopeMatches` (`:2224-2232`) ne lit que `row.commercialId`, `row.campaignId`, `row.centreId`. **La clé couvre exactement les entrées.** 157- le corps de `monthlyFormulaRuleFor` ne consomme que `rules`, `date` et `centreId`. **La clé couvre exactement les entrées.** 158- le cache est porté par l'identité de l'objet `rules` et sa durée de vie est bornée à un build (`finally`, `:684`), ce qui interdit une fuite entre deux utilisateurs ou deux rafraîchissements — le commentaire de l'auteur le dit et le code le fait. 159 160**Verdict : aucun effet sur un montant.** C'est une optimisation correcte. 161 162**Mais elle enferme le défaut du constat 7.** `.filter(activeRow)` précède toujours `.filter((rule) => effectiveOn(rule, date))` à l'intérieur de la fermeture mémoïsée (`:2327-2328`). Le constat 7 — une règle désactivée disparaît de tout le passé — est **préservé verbatim**, désormais derrière un cache. 163 164### C10 · `outsideCentreReceived` : le dénominateur ne change pas, mais le système mesure enfin ce qui le pollue · `FAIT` 165 166Un compteur neuf apparaît (`:485`, `:1332`, `:1424`, `:1642`, `:1972`, `:2019`), alimenté quand `dimension.outsideCentreAttribution` est vrai — c'est-à-dire quand le centre du commercial diffère du centre de rattachement de la ligne (`:2153-2156`). 167 168**Le dénominateur de la transformation est inchangé, et je l'ai vérifié des deux côtés :** 169 170- l'accumulation `row.received += selected.receivedCount ?? 1;` est **identique** entre 0908 (`:1551`) et 0914 (`:1641`) ; le nouveau compteur est ajouté **à côté**, sur la ligne suivante, jamais en soustraction ; 171- `transformation_rate: received ? transformationNumerator / received * 100 : 0` (`:2657`) et `transformationRate: received ? mainSales / received * 100 : 0` (`:2748`) sont inchangés. 172 173**Ce que ce changement établit, en revanche, et qui sert directement le constat 6 :** la plateforme **mesure désormais** la part des appels du dénominateur qui sont attribués hors du centre — et **continue de diviser par le `received` complet**. Les deux définitions concurrentes de la transformation subsistent dans le même fichier : `performanceForPeriod` (`:2657`, numérateur configurable `main_sales` ou `energy_contracts`, `:2639`) et `performanceByCentre` (`:2748`, numérateur toujours `mainSales`). **La donnée qui permettrait de chiffrer le constat 6 existe maintenant dans le code ; elle n'entre dans aucune formule.** 174 175--- 176 177## 6. Le fait neuf le plus lourd du corpus : `flexylead-sync` peut supprimer des ventes 178 179Le cadrage demandait si l'une des trois fonctions neuves « touche à l'argent ». Une recherche par mots-clés répond non : `flexylead-sync.mjs` ne contient ni `prime_montant`, ni `offer_economic_rules`, ni `production_cost_rules`. **C'est la mauvaise question.** Cette fonction ne modifie aucun montant : elle peut **supprimer la ligne qui le porte**. 180 181### C11 · `flexylead-sync` écrit la table `ventes` avec la suppression activée par défaut · `FAIT` 182 183`backend/flexylead-sync.mjs` — 105 Ko, **absent de `functions/`**, absent des deux manifestes, absent de `workflows/`, absent de l'instantané 0908 **et** de `flexylead-crm` (`888cf02`). 184 185Son en-tête déclare l'intention (`:16-18`) : *« Upsert every sync-managed table (centres, offres, grilles, criteres, ventes, appels) by local_id — inserts new rows, updates rows whose diff-fields changed, **deletes rows that no longer exist upstream** »*. 186 187La mécanique : 188 189- `upsertTable` porte `deleteMissing = true` **par défaut** (`:1991`) ; la suppression est exécutée `:2049` (`rows_delete`). 190- L'ensemble de référence est `loadAllRows(ctx, 'ventes')` — **non filtré**, pagination simple (`:1947-1959`). 191- Les lignes candidates viennent de `buildVentes`, qui n'itère que `SHEET_MAPPINGS` (`:1624-1631`), avec `local_id = ${centreId}::${tab}::${++seq}` (`:1670`) — **un numéro d'ordre positionnel**. 192- L'appel sur `ventes` (`:2393-2394`) ne passe **que** `diffFields` : 193 194```js 195deltas.ventes = await upsertTable(context, 'ventes', ventes, 196 { diffFields: ['etat', 'date_activation', 'appel_id', 'score', 'client', 'iban', 'commercial', 'date_vente', 'offre_id'] }); 197``` 198 199`deleteMissing` n'est pas surchargé → il vaut **`true`**. Les quatre tables voisines, elles, sont explicitement protégées : `centres` (`:2381`), `offres` (`:2388`), `grilles` (`:2390`), `criteres` (`:2392`) portent toutes `deleteMissing: false`. **`ventes` et `appels` sont les deux seules laissées au défaut.** 200 201### C12 · C'est bien la même table `ventes` que celle qui porte les primes · `FAIT` 202 203La table `ventes` a **41 colonnes** (`functions/test-support/table-schema.json`, bloc `ventes`) et porte **à la fois** les colonnes issues des feuilles (`contrat_numeros`, `score`, `iban`, `pdl_pce`, `etat`) **et** les colonnes d'argent : `prime_montant`, `prime_devise`, `prime_rule_id`, `revenue_cents_snapshot`, `revenue_currency`, `revenue_rule_id`, `economic_role`. 204 205Les ventes créées par le CRM portent `local_id = vente:${commercial.id}:${requestId}:${index}:${role}` (`backend/flexylead-create-sales.mjs:716`) — **un format que `buildVentes` ne régénère jamais.** Toute vente créée dans le CRM est donc, par construction, « absente en amont » du point de vue de la synchronisation. 206 207### C13 · Le chemin de mise à jour est innocenté ; seul le chemin de suppression détruit · `FAIT` 208 209Discipline appliquée : séparer ce qui abîme un montant de ce qui n'y touche pas. 210 211`upsertTable` pousse `fields: r`, c'est-à-dire **la ligne issue de la feuille**, qui ne contient aucune colonne de prime. Si `rows_update` remplaçait la ligne, toute mise à jour effacerait la prime. **Ce n'est pas le cas :** `flexylead-update-sale.mjs:167-171` transmet un sous-ensemble `fields`, puis relit la ligne et vérifie que les liens protégés n'ont pas changé (`assertProtectedFieldsUnchanged`, `:650-654`) — une séquence qui n'a de sens que sur une sémantique de **patch partiel**. 212 213**Donc : une mise à jour par la synchronisation ne détruit pas la prime. Une suppression détruit la ligne entière.** 214 215### C14 · Le contrôle à deux sens : la fonction remplacée protégeait explicitement ce que la nouvelle laisse ouvert · `FAIT` — c'est le constat le plus solide de ce rendu 216 217J'ai cherché l'antériorité avant de conclure. `flexylead-sync.mjs` se déclare « port of the local sync.js into one Apteva function » (`:1`). Son prédécesseur fonctionnel, `flexylead-sync-local-google.mjs` (5 699 lignes, 234 Ko), est **toujours présent** et **byte-à-byte identique** entre 0908 et 0914. 218 219Ce prédécesseur porte le **même** `upsertTable` avec le **même** défaut `deleteMissing = true` (`flexylead-crm-0908/backend/flexylead-sync-local-google.mjs:3603`) — et il le **surcharge à `false` sur `ventes` à ses cinq points d'appel sans exception** : `:4664`, `:4695`, `:4885`, `:5551`. 220 221Mieux : sa liste `diffFields` pour `ventes` (`:5551`) inclut explicitement **`prime_montant`, `prime_devise`, `prime_rule_id`**. La nouvelle fonction a retiré ces trois champs de la liste **et** la protection contre la suppression. 222 223**Cause → effet :** une fonction neuve écrit `ventes` sans le garde-fou que la fonction qu'elle remplace posait délibérément partout. 224**Effet → cause :** si des ventes venaient à disparaître, la signature serait une suppression en masse des `local_id` ne commençant pas par `${centreId}::`, sans trace — `rows_delete` n'écrit aucun journal, et `ventes` est déjà la seule table de la chaîne d'argent où l'effacement définitif est autorisé (constat T3 du 09/09). 225 226### C15 · Le numéro d'ordre positionnel est un second mécanisme, indépendant du premier · `FAIT` sur le code, `SUGGÉRÉ` sur l'effet 227 228`local_id = ${centreId}::${tab}::${++seq}` est un **rang dans la feuille**, non un identifiant stable. Insérer une ligne au milieu d'un onglet « Suivi des ventes » décale le rang de **toutes les lignes suivantes**. Leurs anciens `local_id` deviennent « absents en amont » → **supprimés** ; les nouveaux sont **insérés**, avec une charge utile (`:1669-1691`) qui ne contient **ni `prime_montant`, ni `prime_devise`, ni `prime_rule_id`** → colonnes nulles. 229 230Et il n'existe aucun rattrapage : `flexylead-reconcile-sale-revenue` ne renseigne que le CA fournisseur manquant, jamais la prime (commande F, `analyse/preuves-argent-2026-09-09.md:143-145`). **Une prime perdue par ce chemin l'est définitivement.** 231 232### C16 · Ce que je ne peux pas établir, et qui borne tout le §6 · limite déclarée 233 234**Je ne peux pas établir que cette fonction a jamais tourné contre la production.** Elle est absente des deux manifestes, absente de `workflows/`, absente de `functions/`. Son handler mentionne un appel par cron (`:2069-2070`, *« The cron's HTTP caller times out at ~30s but the sync runs ~150s server-side »*), ce qui atteste une **intention** de planification, pas une planification constatée. 235 236Deux hypothèses concurrentes, aucune écartée : 237 2381. **Elle tourne.** Alors des ventes créées dans le CRM ont pu être supprimées, et l'effet serait massif et visible — ce qui plaide plutôt contre. 2392. **Elle ne tourne pas encore** (portage en cours, l'ancienne fonction de 234 Ko restant en service — elle est toujours là, inchangée). C'est cohérent avec son absence des manifestes et avec la coexistence des deux importateurs. 240 241**L'hypothèse 2 est la plus probable au vu du corpus.** Je classe donc l'exposition du §6 en **`HYPOTHÈSE` quant à sa réalisation**, et en **`FAIT` quant au défaut du code**. C'est précisément le type de constat qui doit être tranché avant une bascule, pas après. 242 243--- 244 245## 7. Chiffrage de l'exposition 246 247Volumes de référence, inchangés : 200 contrats énergie/mois par centre, barème observé **68 €/contrat** (`analyse/contre-contrat-2026-09-09.md:13`), 306 opportunités/mois (ZC Call). 248 249| # | Écart | Source | Chiffrage à 200 contrats/mois | Sens | Statut au 14/09 | 250|---|---|---|---|---|---| 251| X1 | Formule partielle → défauts du barème B | `manage-production-economics.mjs:343`, `:384-396`, `:413-422` | **+22 €/contrat = 4 400 €/mois, 52 800 €/an** | faveur du centre | **intact** | 252| X2 | `maximum_amount` omis → plafond 100 € | `:385` | −4 000 €/mois sur un barème voulu à 120 € | défaveur du centre | **intact** | 253| X3 | `ineligible_amount` omis → 0 € | `:395` | −13 600 €/mois sur une période sous le seuil | défaveur du centre | **intact** | 254| X4 | Sensibilité de `base_amount` | `:391` | 1 € = **200 €/mois = 2 400 €/an** par centre | selon le sens | **intact** | 255| X5 | Fork des coûts par l'apply | `:490-491` (13/09) | 2 754 €/mois d'assiette figée ; 612 €/mois si le global bouge | défaveur de FLEXYLEAD | **intact** | 256| **X7** | **Suppression d'un contrat énergie par `flexylead-sync`** | `flexylead-sync.mjs:1991`, `:2393`, `:2049` | **−68 € par contrat supprimé**, plus la prime du commercial, définitivement | défaveur du **centre** et du **commercial** | **neuf** | 257| **X8** | **Déplacement d'un palier par variation du dénominateur** | `remuneration-query.mjs:2662-2664` | les taux Météore et ConsoPilote se calculent sur `energyContracts \|\| mainSales` ; supprimer des contrats bouge **numérateur et dénominateur**. Un palier ±5 € franchi = **±5 €/contrat × 200 = ±1 000 €/mois** | selon le sens | **neuf** | 258| **X9** | **Perte de prime par recalage des rangs** | `flexylead-sync.mjs:1670`, `:1669-1691` | toute vente située après une insertion dans un onglet est recréée avec `prime_montant = null`, **sans rattrapage possible** | défaveur du **commercial** | **neuf** | 259 260X7 à X9 ne sont chiffrables au-delà de l'unité qu'avec le nombre de ventes concernées, que seule la base donne (V6, V7 ci-dessous). 261 262--- 263 264## 8. Les autres fonctions neuves 265 266| Fonction | Tables écrites | Argent ? | 267|---|---|---| 268| `leadbyte-prospect-dispatcher.mjs` | `prospects`, `campaigns` | **Non directement.** Mais `prospects` alimente le dénominateur quand `opportunity_basis` vaut `assigned_prospects` (`manage-production-economics.mjs:400`). À router vers `audit-routage`. `SUGGÉRÉ`. | 269| `flexylead-discard-missed-call.mjs` | `appels`, `commerciaux`, `commercial_group_members`, `work_routing_assignments` | **Non directement.** Mais `appels` alimente `received`, dénominateur de la transformation. À router vers `audit-routage`. `SUGGÉRÉ`. | 270| `process-appel.mjs` (1 609 l., **neuf dans `backend/`, absent de `functions/`**) | **`rapports`** (`:1584`, `:1587`), `appels` | **Oui, indirectement et sérieusement.** `rapports` alimente `average_ai_score`, qui déclenche le palier « score IA ≥ 90 % → +5 € » (`defaultFormula`, `:421`) et les malus qualité du barème C. Or `workflows/process-appel.yaml:11` déclenche **`process-appel-db-only`** (3 972 l.), pas celui-ci. **Deux implémentations distinctes écrivent la table qui pilote un palier.** À router vers `audit-qualite-ia`. `FAIT` sur la coexistence, `HYPOTHÈSE` sur laquelle s'exécute. | 271 272**Signalement hors périmètre, à router vers `audit-integrations` :** `backend/flexylead-sync.mjs:31` porte une clé d'API Composio **en clair** (`apiKey: 'ak_cpsw-…'`) et `:34` un identifiant d'utilisateur. Consigné factuellement, sans recommandation, conformément au cadre. 273 274--- 275 276## 9. Reprise des constats 6 à 9 contre le déployé du jour 277 278| # | Constat (famille 2) | Verdict au 14/09 | Preuve | 279|---|---|---|---| 280| **6** | Aucun montant dû à un centre n'est figé ; tout est recalculé à l'ouverture de l'écran | **Tient** | Le bloc `evaluateMonthlyFormula` → `saleRemunerationForEntry` est identique (C7). Aucune écriture ajoutée sur ce chemin. **Réserve :** je n'ai pas rejoué la commande A (comptage des écritures sur les 8 fichiers) sur le corpus 0914 — voir §11. | 281| **7** | Supprimer une règle l'efface de tout le passé (l'état actif est jugé avant les dates) | **Tient, verbatim** | `monthlyFormulaRuleFor` : `.filter(activeRow)` puis `.filter(effectiveOn)`, `remuneration-query.mjs:2327-2328`, désormais à l'intérieur du cache (C9). | 282| **8** | Asymétrie des dates d'effet : barème centre verrouillé et versionné, prime du commercial rétroactive et écrasée | **Tient** | `validatedEffectiveFrom` et `versionRule` intacts (C8) ; `flexylead-manage-offer-catalog.mjs` inchangé hors encodage. | 283| **9** | Le détail du calcul par contrat est produit puis jeté | **Tient** | `saleRemunerationForEntry` identique au caractère près (C7). | 284 285**Les quatre constats de la famille 2 survivent intégralement au nouveau déployé.** Aucun n'est corrigé ; aucun n'est aggravé par l'adaptateur. 286 287**En revanche, la garantie probatoire qui les accompagnait le 09/09 est perdue.** Le rapport du 09/09 pouvait écrire : *« les quatre constats décrivent ce qui tourne, pas seulement la source de travail »*, parce que les sept fichiers du périmètre étaient **identiques** entre `functions/` et `backend/`. Aujourd'hui ils **divergent tous** (72, 104, 70, 74, 69 lignes d'écart), et la phrase du README qui fondait la lecture a disparu. **La phrase du 09/09 doit être retirée ou requalifiée** : elle n'est plus soutenue par le corpus. 288 289--- 290 291## 10. Sort de mes six vérifications du 13/09 292 293| # | Objet | Tranchée par ce corpus ? | 294|---|---|---| 295| **V1** | Chronologie réelle des barèmes en base | **Non.** Exige des lignes de production. | 296| **V2** | Lignes de `stats_periods`, bornes et auteurs | **Non.** Exige des lignes de production. | 297| **V3** | `flexylead-admin-mcp` est-il déployé, avec quel déclencheur | **Non — et je conteste la prémisse inverse.** Voir C2 : la présence dans `backend/` s'explique par une copie de la source du 08/09. Reste `HYPOTHÈSE`. | 298| **V4** | Présence de `FLEXYLEAD_ACTOR_ASSERTION_SECRET` | **Oui.** 12 fichiers, dont la cible (`:728`) et l'appelant (`:1383`). Voir C6. | 299| **V5** | Transcription du fil de l'agent n°170 | **Non.** | 300| **V6** | Lignes `scope_type = "centre"` récentes | **Non.** | 301| **V7** | *(neuve)* Nombre de lignes `ventes` dont le `local_id` ne commence pas par `${centreId}::`, et existence de suppressions récentes sur `ventes` | **Non** — c'est la vérification la plus urgente du lot (§6). | 302| **V8** | *(neuve)* `flexylead-sync` est-elle déployée et planifiée, et l'ancienne `flexylead-sync-local-google` tourne-t-elle encore ? | **Non.** Tranche C16. | 303 304--- 305 306## 11. Ce que je n'ai pas pu établir 307 3081. **Si `backend/` est réellement le déployé.** La seule pièce à l'appui est le message de commit. C2 montre que le répertoire n'est pas homogène, ce qui affaiblit la déclaration sans la réfuter. 3092. **Si `flexylead-sync` s'exécute.** Décisif pour X7–X9. 3103. **Laquelle des deux implémentations `process-appel` s'exécute.** Décisif pour la fiabilité du palier « score IA ». 3114. **Le nombre de ventes exposées à la suppression**, donc le montant de X7 au-delà de l'unité. 3125. **Le contrôle d'écriture (commande A) rejoué sur 0914.** J'ai établi l'identité du bloc de calcul, pas l'absence d'écriture ailleurs dans les six fichiers de pilotage. À compléter avant tout usage opposable du constat 6. 313 314--- 315 316## 12. Prescriptions, avec rayon d'impact 317 318Aucune ne s'exécute en phase 1. Formulées pour `plan-acte2`. 319 320| # | Geste | Rayon d'impact | 321|---|---|---| 322| **P6** | **Avant toute bascule : déterminer si `flexylead-sync` est déployée et planifiée.** Si oui, poser `deleteMissing: false` sur `ventes` et `appels`, comme le fait son prédécesseur à ses cinq points d'appel. | Touche `flexylead-sync.mjs:2393` et `:2396`. Lecteurs de `ventes` : tout le moteur de rémunération, les écrans commerciaux, la facturation fournisseur. **Ne modifie aucun montant ; empêche la disparition de lignes qui en portent.** Le prédécesseur prouve que c'est le comportement voulu. | 323| **P7** | Rendre `local_id` des ventes issues des feuilles **non positionnel** (clé métier : numéro de contrat + onglet). | Touche `buildVentes` (`:1670`) et, par ricochet, l'appariement ventes↔appels. **Migration de clés : à ne pas faire sans reprise de l'existant.** Ferme X9. | 324| **P8** | Trancher laquelle des deux `process-appel` fait autorité et retirer l'autre. | Écrit `rapports`, lu par `average_ai_score` → palier ±5 €/contrat, soit **1 000 €/mois** à 200 contrats. À arbitrer avec `audit-qualite-ia`. | 325| **P9** | Rétablir dans le README une phrase qui qualifie `backend/`, ou cesser de livrer ce répertoire. | Aucun effet sur le système. Effet sur l'auditabilité : sans elle, **aucun constat ne peut être qualifié d'« établi sur le déployé »**, ce qui retire sa portée à la garantie du 09/09. | 326| P1–P5 | *(inchangées, rendu du 13/09)* | Notamment **P1 : ne pas « réparer » `:546`** sans décision préalable sur le garde-fou — C5 et C6 confirment que la correction est d'une ligne et qu'elle armerait le chemin d'écriture du barème. | 327 328--- 329 330## 13. Contre-lecture — ce qui, dans le corpus, pourrait me contredire 331 3321. **C2 est mon constat le plus conséquent et il repose sur un raisonnement de provenance, pas sur une preuve directe.** Si Marco a durci la production **puis** copié le MCP depuis sa source de travail dans le même instantané, le fichier serait à la fois déployé **et** byte-identique à la source du 08/09 — et ma conclusion serait fausse. Je ne peux pas écarter ce scénario. **Je ne conclus donc pas « le MCP n'est pas déployé » : je conclus que le corpus ne permet pas de dire qu'il l'est**, ce qui est une position plus faible que celle du cadrage, et volontairement. 3332. **Le §6 pourrait être un procès d'intention.** `flexylead-sync` est peut-être un portage inachevé, jamais déployé, et son `deleteMissing` serait corrigé avant mise en service. C'est l'hypothèse que je juge la plus probable (C16). **Ce qui reste vrai dans tous les cas :** le fichier est arrivé dans le répertoire présenté comme la production, et la fonction qu'il porte a perdu un garde-fou que son prédécesseur posait cinq fois. C'est un constat sur la livraison, pas seulement sur le code. 3343. **J'ai donné raison à l'adaptateur.** C'était l'occasion de conclure que le durcissement avait faussé un calcul. Il ne l'a pas fait : le bloc de rémunération est identique au caractère près, et la mémoïsation — que j'ai attaquée en cherchant la collision de clé — est correcte. Je l'écris. 3354. **X1 reste mon chiffrage le plus exposé.** Il suppose un apply partiel. Le 09/09, une coïncidence arithmétique de même nature a déjà été invalidée (`analyse/contre-contrat-2026-09-09.md:46`). Il se tranche par V1 et V5, toujours ouvertes. 3365. **Aucun chiffrage ne s'appuie sur les captures du 26/08** désavouées par Marco. 337