Preuve · rapport

famille2-argent-vs-deploye-2026-09-14.md

Rapport de vérification de l'audit, texte brut · sur GitHub (dépôt privé, accès requis)

337 lignes · ← retour à l'audit · catalogue des preuves

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