1# Preuves de la famille 3 « La notation qualité » — audit-v3.html 2 3Auditeur : `audit-qualite-ia` · 09/09/2026 · **lecture seule**, aucun accès serveur, aucun appel réseau, aucun code applicatif exécuté. 4 5## Périmètre traité 6 7- `audit-v3.html` § « Les constats », famille 3, ses 4 constats (titres, paragraphes, encarts correctifs) 8- Carte étage 4, cases « Le texte de l'appel » et « La note de qualité » 9- § « Ce qui tient » : la phrase sur la réponse malformée 10 11Dépôts lus : `code/flexylead-crm-0908/` (`730a5e79`), `code/flexylead-crm/` (`888cf02`), `code/apteva-platform/`. 12 13## Parité entre les deux livraisons — vérifiée 14 15Le moteur de notation est **toujours identique** entre les deux livraisons : 16 17``` 18md5 code/flexylead-crm/backend/process-appel-db-only.mjs = 9d7b7dc2aadd231836f04e480eecaa22 19md5 code/flexylead-crm-0908/backend/process-appel-db-only.mjs = 9d7b7dc2aadd231836f04e480eecaa22 20md5 code/flexylead-crm-0908/functions/process-appel-db-only.mjs = 9d7b7dc2aadd231836f04e480eecaa22 21``` 22 23`src/dashboard/components/RapportViewer.tsx` est également identique entre les deux dépôts (`diff -q` sans sortie). 24 25**Trois différences apportées par la livraison du 08/09, toutes structurantes pour cette famille :** 26 271. `code/flexylead-crm-0908/config/*.json` — les cinq grilles chiffrées. **Ce répertoire n'existe pas dans `code/flexylead-crm/`.** Tous les chiffres de barème du document en viennent. 282. `functions/process-appel-db-only.prod-active-before-prompt-fix.mjs` — une copie de moteur **antérieure** (2 338 lignes contre 3 962). 293. `functions/` (sources de travail) et `backend/` (« snapshots de sources déployées », README.md ligne 11 et ligne 156) contiennent **le même** `process-appel-db-only.mjs`. 30 31--- 32 33## Tableau des phrases 34 35| # | Phrase exacte du document | Verdict | Fonction(s) et grille(s) à nommer dans le texte | Preuve `fichier:ligne` | Texte de remplacement si nécessaire | 36|---|---|---|---|---|---| 37| 1 | « Le texte de l'appel ne dit pas qui parle, et onze contrôles y cherchent des phrases » (titre) | **VRAI** | `deepgramTranscribe`, `enforceParsedBusinessRules` | `flexylead-crm-0908/functions/process-appel-db-only.mjs:945-958` (aucun `diarize`) · `:2419-2628` (11 tests) | — | 38| 2 | « La transcription est demandée sans séparer les interlocuteurs : client et commercial sont mélangés dans un même bloc. » | **VRAI**, à préciser | `deepgramTranscribe` (voie qualité) et `transcribe` de `flexylead-transcribe-telephony-recording` (voie téléphonie) | `functions/process-appel-db-only.mjs:951-957` : `model:'nova-3', smart_format, punctuate, paragraphs` — **pas de `diarize`** · `backend/flexylead-transcribe-telephony-recording.mjs:200-207` : `diarize:true` mais `:213-214` ne lit que `alternatives[0].transcript`, chaîne plate sans étiquette · les deux écrivent la même colonne `appels.transcript_text` (`process-appel-db-only.mjs:1750` / `flexylead-transcribe-telephony-recording.mjs:151`) | Préciser : « La transcription est demandée sans séparation des interlocuteurs par `process-appel-db-only`. La seconde voie, `flexylead-transcribe-telephony-recording`, la demande bien mais n'en lit que le texte brut : dans les deux cas la colonne écrite ne porte aucune étiquette. » | 39| 3 | « Onze contrôles automatiques y cherchent des tournures. » | **VRAI — compté** | `enforceParsedBusinessRules`, et ses onze sondes : `transcriptShowsAcceptedConsoDataAuthorization`, `transcriptShowsCompletedProtectedIbanProcedure`, `transcriptShowsFourteenDayWithdrawalNotice`, `transcriptShowsConsoPiloteSubscriptionCompletion`, `transcriptShowsAcceptedCnilNotice`, `transcriptShowsOutstandingBalanceVerification`, `transcriptShowsFourteenDayWithdrawalNotice` (2ᵉ emploi), la sonde interne `transcriptShowsEnergyPresentation`, `transcriptShowsIbanDigitsAfterPause`, `transcriptShowsProtectedIbanCollection`, `transcriptShowsCompletedMintValidation` | `functions/process-appel-db-only.mjs` : C014 `:2448`, C010 `:2453`, C011 `:2458`, C007 `:2463`, M003 `:2468`, M013 `:2488`, M014 `:2493`, M001 `:2536-2538`, M017 `:2548` et `:2555`, M018 `:2574` — **onze tests exactement** | — | 40| 4 | « Un client qui dit « je n'ai pas d'impayé » fait créditer le commercial à sa place — cette phrase est littéralement dans la règle de recherche. » | **VRAI** | `transcriptShowsOutstandingBalanceVerification`, critère M013 « Vérification des impayés » de la grille `mint_energie_v2` | `functions/process-appel-db-only.mjs:1005` : `/(?:avez[- ]?vous|vous avez|pas|aucun|aucune)[^.?!]{0,100}(?:impaye|retard de paiement|dette)/i` — l'alternance `pas` … `impaye` matche l'énoncé du client · effet : `:2488-2492` verdict forcé `OK`, `score = bareme` | — | 41| 5 | « Sur une grille, trente-trois points sur cent se décident ainsi » | **VRAI — recalculé** | grille `mint_energie_v2` ; critères M001 (3), M003 (3), M013 (5), M014 (5), M017 (7), M018 (10) | `config/mint_quality_v2.json` (somme des barèmes du fichier = 100, contrôlée par `functions/mint-selectra-quality-v2-config.test.mjs:18`) — 3+3+5+5+7+10 = **33** | — | 42| 6 | « sur une autre, vingt-cinq » | **VRAI — recalculé** | grille `conso_pilote_v2` ; critères C007 (5), C010 (5), C011 (10), C014 (5) | `config/conso_pilote_quality_v2.json` (somme = 100) — 5+5+10+5 = **25** | — | 43| 7 | (correctif) « la plateforme écarte aujourd'hui le détail mot à mot où vivent ces étiquettes. Activer l'option ne suffira pas. » | **VRAI** | `executeIntegrationToolOnce` et `omitPath` côté plateforme ; `compactDeepgramSentenceTimings` côté CRM | `apteva-platform/integrations/src/apps/deepgram.json` — `response_omit` contient `results.channels[].alternatives[].words` ; la description de l'outil `listen` dit « per-word timestamps are always stripped server-side » · `apteva-platform/server/connections.go:2579-2582` (application du retrait, dans `executeIntegrationToolOnce`, `connections.go:1954`) · `functions/process-appel-db-only.mjs:982-993` : `compactDeepgramSentenceTimings` ne conserve que `start`, `end`, `text` et jette `paragraph.speaker` | — | 44| 8 | (correctif) « deux d'entre eux retirent des points » | **VRAI** | M017 via `transcriptShowsIbanDigitsAfterPause` (score ramené à la moitié du barème) et C007 via `transcriptShowsConsoPiloteSubscriptionCompletion` (score 0 et note finale ramenée à 0/100) | `functions/process-appel-db-only.mjs:2548-2553` (`e.score = Number(e.bareme||0)/2`) · `:2463-2467` (`e.score = 0`, `forcing_confirmed = true`) | — | 45| 9 | « Une grille en mode malus est additionnée au lieu d'être déduite » (titre) | **À BORNER** | `calculateEvaluationScore`, `normalizeScoringMode`, `gridBaseScore` ; grilles `hop_selectra_universelle_v1` et `selectra_compensation_carbone_v1` | Le moteur archivé comme déployé **connaît** le mode malus : `backend/process-appel-db-only.mjs:1233-1256` et `functions/process-appel-db-only.mjs:1233-1256` | « Une grille en mode malus est additionnée par la correction manuelle — et l'était par le moteur avant son correctif » (voir #11 et #12) | 46| 10 | « Une des grilles livrées déclare fonctionner par retrait de points sur une note de départ. » | **VRAI, mais incomplet** | grilles `hop_selectra_universelle_v1` **et** `selectra_compensation_carbone_v1` | `config/hop_selectra_quality_v1.json` : `scoring_mode: "deduction"`, `base_score: 100`, 25 critères, malus max **69,5** · `config/hop_selectra_quality_v2.json:75-76` : `selectra_compensation_carbone_v1`, `deduction`, base 100, 9 critères, malus max **14** · assertions : `functions/hop-selectra-quality-config.test.mjs:9-10`, `functions/mint-selectra-quality-v2-config.test.mjs:44-49` | « **Deux** des grilles livrées déclarent fonctionner par retrait de points sur une note de départ de 100 : `hop_selectra_universelle_v1` (malus maximum 69,5) et `selectra_compensation_carbone_v1` (malus maximum 14). » | 47| 11 | « La version du moteur réellement en service ne connaît ni la note de départ, ni le retrait. Elle additionne. » | **FAUX en l'état — À BORNER** | `calculateEvaluationScore` (présent), `process-appel-db-only.prod-active-before-prompt-fix` (absent) | `backend/process-appel-db-only.mjs:1244-1256` contient `calculateEvaluationScore` ; `backend/` est décrit comme « deployed-source snapshots » (`README.md:11`, `README.md:156`) et ce fichier est **identique** à la source de travail. À l'inverse, `functions/process-appel-db-only.prod-active-before-prompt-fix.mjs` ne contient ni `calculateEvaluationScore`, ni `normalizeScoringMode`, ni `base_score` (`grep -c` = 0) et somme les scores en `:1795-1796`. Ce fichier ne porte aucune date : son nom dit « avant le correctif de prompt », donc **un état antérieur**. Aucune pièce du dépôt n'établit lequel des deux est actif. | « Une copie de moteur livrée sous le nom `process-appel-db-only.prod-active-before-prompt-fix` ignore la note de départ et additionne. La copie archivée comme source déployée, elle, applique bien le retrait. **Rien dans la livraison ne dit laquelle tourne** : c'est à faire trancher par Marco, et cela commande le sort de toutes les notes Selectra déjà rendues. » | 48| 12 | « Un appel qui devrait valoir 90 sur 100 est rapporté 40 sur 50. » | **FAUX** | — | **Aucune grille du dépôt n'a un barème cumulé de 50.** Barèmes cumulés : `mint_energie_v2` 100, `conso_pilote_v2` 100, `free_v3` 100, `hop_selectra_universelle_v2` 100, `hop_selectra_universelle_v1` **69,5**, `selectra_compensation_carbone_v1` **14** (calculés depuis `config/*.json`) | « Sur `hop_selectra_universelle_v1`, un appel qui devrait valoir 90 sur 100 est rapporté **59,5 sur 69,5** — soit 85,6 % au lieu de 90 %. Sur `selectra_compensation_carbone_v1`, un appel qui devrait valoir 96 sur 100 est rapporté **10 sur 14**, soit 71,4 %. » | 49| 13 | (correctif) « Déployer la version du moteur qui connaît les deux modes. Elle est dans le dossier » | **VRAI**, mais dépend de #11 | `calculateEvaluationScore`, `buildEvalSystem` | `functions/process-appel-db-only.mjs:1244-1256` et `:1840-1843` (le prompt système lui-même bascule en mode malus : « le serveur calcule ensuite la note finale en déduisant de la note de départ les points perdus ») | À conditionner : « si c'est bien la copie antérieure qui tourne » | 50| 14 | « Corriger une note à la main la fait tomber sous le seuil de paiement » (titre) | **VRAI, sur les deux grilles en mode malus** | `save` de `RapportViewer` et `correctRapport`, contre `calculateEvaluationScore` ; grilles `hop_selectra_universelle_v1` et `selectra_compensation_carbone_v1` | `src/dashboard/components/RapportViewer.tsx:125` : `const score_total = Math.round(corrected.reduce((s, e) => s + (Number(e.score) || 0), 0) * 100) / 100;` — somme brute, sans mode ni note de départ | — | 51| 15 | « il garde la note d'origine » | **À BORNER** | `save` de `RapportViewer` | `RapportViewer.tsx:118-119` : `ai_verdict: e.ai_verdict ?? e.verdict, ai_score: e.ai_score ?? e.score` — l'original est conservé **ligne par ligne, et seulement à la première correction**. `correctRapport` (`src/dashboard/api/client.ts:7210-7219`, appelée en `RapportViewer.tsx:126`) n'écrit que `evaluations`, `score_total`, `modified_time` : **le score global d'origine n'est conservé nulle part** | « il garde le verdict et le score d'origine de chaque ligne — mais pas la note globale d'origine, qui est écrasée » | 52| 16 | « il note qui a corrigé et quand, il affiche un bandeau » | **VRAI** | `save` de `RapportViewer` ; contrôle d'écriture par `flexylead-admin-data-mutation` | `RapportViewer.tsx:120-121` (`corrected_by`, `corrected_at`) · `:157-165` (badge « Corrigé par … ») · `:208` (« Mode correction ») · `backend/flexylead-admin-data-mutation.mjs:21` : la politique `rapports` n'autorise que `evaluations`, `score_total`, `modified_time`, `status`, `updated_at_iso` — **`score_max` n'est pas modifiable**, donc le dénominateur reste 100 · journal dans `admin_mutation_events` (`:3`) | — | 53| 17 | « le moteur et la correction manuelle ne calculent pas de la même façon » | **VRAI, à borner** | `calculateEvaluationScore` (serveur) contre `save` de `RapportViewer` (écran) ; `grilleScoringSummary` existe mais n'est pas appelée | `functions/process-appel-db-only.mjs:1244-1256` contre `RapportViewer.tsx:125` · `src/dashboard/lib/grilleScoring.ts:3-9` connaît le mode malus mais n'est employée que par `src/dashboard/pages/Criteres.tsx:128` — `grep -c "grilleScoring" src/dashboard/components/RapportViewer.tsx` = **0**. Ligne à ligne les deux concordent (`reconcileEvalScore`, `process-appel-db-only.mjs:2059-2077`, et `scoreForVerdict`, `src/dashboard/lib/scoring.ts:10-17`) : **l'écart est uniquement à l'agrégation, et uniquement en mode malus** | Préciser : « … dès que la grille fonctionne par malus. L'écran possède pourtant la fonction qui sait le faire, `grilleScoringSummary` ; l'écran de correction ne l'appelle pas. » | 54| 18 | « un appel parfait passe de 100 à 69,5 après une simple correction » | **VRAI — recalculé** | grille `hop_selectra_universelle_v1` ; `calculateEvaluationScore` puis `save` de `RapportViewer` | Appel sans défaut : `calculateEvaluationScore` → `score_total = 100`, `score_max = 100` (`process-appel-db-only.mjs:1250-1255`). Correction : somme brute des barèmes = **69,5** (`config/hop_selectra_quality_v1.json`, valeur reprise telle quelle dans `src/dashboard/lib/grilleScoring.test.ts:7`), `score_max` inchangé à 100 → **69,5 / 100** | Ajouter que **la seule modification d'un commentaire suffit** : `RapportViewer.tsx:110-112` incrémente `changes` sur un changement de commentaire seul, et déclenche le recalcul | 55| 19 | « sur une autre, de 100 à 14 » | **VRAI — recalculé** | grille `selectra_compensation_carbone_v1` | 9 critères, barèmes cumulés **14** (`config/hop_selectra_quality_v2.json`, `compensation_criteria`) ; `score_max` reste 100 → **14 / 100** | — | 56| 20 | « Sous le seuil, la vente cesse d'être rémunérée. » | **À BORNER** | `saleRuleMatches` et `evaluateSaleRemuneration` | `backend/flexylead-app-pilotage-remuneration-query.mjs:2412-2419` et `:2421-2440` : le seuil est **une donnée**, lue dans `condition.threshold` de `sale_rules`, métrique `ai_score`, effet `exclude`. Voir la section « Le seuil de 80 » ci-dessous | « Sous le seuil configuré pour le centre, la vente cesse d'être rémunérée. Ce seuil n'est pas dans le code : il est enregistré dans la formule de rémunération du centre. » | 57| 21 | « si l'appel repasse dans la chaîne, le rapport est réécrit de bout en bout » | **VRAI** | `processOne`, via `exactRapportFirstLookup` puis `rows_update` | `functions/process-appel-db-only.mjs:3565-3585` : `rapportRow` réécrit `evaluations`, `score_total`, `score_max`, `raw_plaintext`, `modified_time`, puis `:3584` `rows_update` sans aucune lecture d'un marqueur de correction | — | 58| 22 | « si l'IA conclut que l'appel ne correspond pas à l'offre, il est supprimé » | **VRAI** | `clearRapportsAndMarkNonEvaluable` | `functions/process-appel-db-only.mjs:2272-2295` : `rows_delete` sur chaque rapport trouvé, sans condition ; déclenché sur `PRODUIT_MISMATCH` (`parseNonEvaluableReason`, `:2133`) | — | 59| 23 | « Rien, dans le moteur, ne sait qu'un humain était passé. » | **VRAI** | `processOne`, `clearRapportsAndMarkNonEvaluable` | `grep -cE "corrected_at\|corrected_by\|ai_verdict\|ai_score" functions/process-appel-db-only.mjs` → **0** | — | 60| 24 | « Le même appel peut recevoir deux notes différentes » (titre) | **VRAI** | `claudeEvaluate` ; `processSweep` / `selectSweepCandidates` ; `evaluateAddonWithRecovery` | `functions/process-appel-db-only.mjs:2016-2041` (appel modèle) · `:2823` et `:2919` (le balayage reprend les `eval_incomplete`, `:2940-2944`) · `:2193-2234` et `:3404` (seconde évaluation du même appel avec une instruction corrective) · côté écran, `siblingAttempts` (`src/dashboard/lib/attempts.ts`) et le badge « Tentative N / M » (`RapportViewer.tsx:151-157`) admettent explicitement plusieurs notes par dossier | — | 61| 25 | « La notation est confiée à un modèle qui ne rend pas deux fois exactement la même réponse. » | **VRAI — non-déterminisme subi, non configuré** | `claudeEvaluate`, intégration `anthropic-api_create_message`, modèle `claude-haiku-4-5` | `functions/process-appel-db-only.mjs:2021-2026` : la charge n'a que `model`, `max_tokens`, `system`, `messages`. `grep -cE "temperature\|seed\|top_p\|top_k"` sur `functions/` **et** `backend/process-appel-db-only.mjs` → **0**. Aucun paramètre d'échantillonnage n'est fixé, aucune graine n'est passée | Ajouter, puisque c'est le levier : « aucune température ni graine n'est transmise à `claudeEvaluate` » | 62| 26 | « Rien n'enregistre la version du modèle ni les instructions qui lui ont été données. » | **VRAI** | `processOne` ; table `rapports` | `functions/test-support/table-schema.json`, table `rapports` : les colonnes sont `score_total`, `score_max`, `evaluations`, `raw_plaintext`, `controles_ia`… — **aucune colonne de modèle ni de prompt** · `functions/process-appel-db-only.mjs:3565-3580` : `rapportRow` n'en écrit aucune | — | 63| 27 | « Deux notes qui se contredisent sur le même dossier ne peuvent pas être départagées. » | **À BORNER** | `latestReportScoresForPeriod` et l'index de `reportScoreForSale` | `backend/flexylead-app-pilotage-remuneration-query.mjs:2380-2397` et `:2577-2597` : **le départage existe et il est mécanique — la note la plus récente par `modified_time` l'emporte**, clé `sale:<id>` sinon `call:<id>`. Ce qui manque, c'est le moyen de dire laquelle des deux est juste | « Le calcul retient d'office la plus récente. Faute d'avoir gardé le modèle et les instructions, personne ne peut dire si c'est la bonne. » | 64| 28 | (correctif) « Les cases existent déjà sur la fiche d'appel ; seuls les programmes de démonstration les remplissent. » | **VRAI** | colonnes `ai_analysis_model` et `ai_analysis_version` de la table `appels` ; écrites par `seed-local-heavy-production`, `seed-staging-lucas-current-day`, `seed-staging-mk0819-current-day` | `functions/test-support/table-schema.json:537,542` (les colonnes) · `scripts/seed-local-heavy-production.mjs:259`, `scripts/seed-staging-lucas-current-day.mjs:114-115`, `scripts/seed-staging-mk0819-current-day.mjs:114-115` (les seuls écrivains) · aucune occurrence dans `backend/` ni dans `functions/process-appel-db-only.mjs` | — | 65| 29 | Carte, « Le texte de l'appel » : « L'enregistrement est envoyé à un prestataire extérieur, qui le transforme en texte. » | **VRAI** | `deepgramTranscribe` ; intégration `deepgram_listen`, modèle `nova-3` | `functions/process-appel-db-only.mjs:946-957` : URL signée par `files_get_url` (30 min), puis `context.integration('deepgram','deepgram_listen', …)` | — | 66| 30 | Carte, défaut : « Le texte ne dit pas qui parle. Le client et le commercial sont mélangés dans un même bloc. » | **VRAI** | `deepgramTranscribe`, `compactDeepgramSentenceTimings` | mêmes preuves que #2 et #7. Indice à charge : `transcriptShowsAcceptedConsoDataAuthorization` prévoit un préfixe `client :` dans la réponse attendue (`functions/process-appel-db-only.mjs:2371`) — préfixe que la transcription ne produit jamais | — | 67| 31 | Carte, « La note de qualité » : « Une intelligence artificielle relit ce texte et passe la grille, critère par critère. » | **VRAI** | `claudeEvaluate`, `buildEvalSystem`, `renderGrilleText`, `buildEvalUser`, `parseRapport` | `functions/process-appel-db-only.mjs:2016` (appel), `:1840` (système), `:1206-1231` (rendu de la grille : une ligne par critère, barème, marque `[CRITIQUE]`), `:1931` (contexte : ventes, offres, transcript, faits vérifiés), `:2629` (relecture ligne à ligne). Le critère **est** transmis avec son barème et son caractère critique ; **`applicable` n'est pas transmis** (`renderGrilleText` ne l'écrit pas) | — | 68| 32 | Carte, défaut : « Onze contrôles cherchent des phrases dans ce texte. Un client qui dit « je n'ai pas d'impayé » fait créditer le commercial à sa place. » | **VRAI** | `enforceParsedBusinessRules`, `transcriptShowsOutstandingBalanceVerification`, critère M013 de `mint_energie_v2` | voir #3 et #4 | — | 69| 33 | « Ce qui tient » : « Quand la réponse de l'IA revient malformée, aucune note n'est écrite plutôt qu'une note fausse. » | **VRAI** | `parseRapport` puis le garde-fou de `processOne`, avec `nextEvalFailure` | `functions/process-appel-db-only.mjs:3512-3534` : si `evaluations.length !== criteres.length` ou `score_total > score_max`, **aucun `rows_insert` sur `rapports`** ; l'appel bascule en `eval_incomplete` ou `eval_failed` via `nextEvalFailure` (`:3777`). Le commentaire du code le dit : « do NOT save a misleading 0/100 rapport » (`:3510`). Le même garde-fou existe dans la copie antérieure (`process-appel-db-only.prod-active-before-prompt-fix.mjs:2213-2226`) | Ce garde-fou est exactement ce qui neutralise le « rapport à 0 critère noté 0/100 » relevé sur pièces — **à dire**, car c'est un correctif déjà écrit | 70 71--- 72 73## Le seuil de 80 — tranché 74 75**Le contradicteur a raison sur la lettre, et tort sur la conclusion.** 76 77- **Aucune constante `80` de rémunération n'existe dans le code.** Le seuil est une donnée : `saleRuleMatches` (`backend/flexylead-app-pilotage-remuneration-query.mjs:2412-2419`) lit `condition.threshold` et `condition.operator` d'une règle `sale_rules`, métrique forcée à `ai_score` ; `evaluateSaleRemuneration` (`:2421-2440`) applique l'effet `exclude` / `multiply` / `set`. La normalisation est `score_total / score_max * 100` (`:2391`). 78- **La forme de la règle est écrite, elle, et validée par un test de non-régression** : `functions/flexylead-app-pilotage-query.test.mjs:1783-1787` porte une règle intitulée en toutes lettres **« Vente non rémunérée sous 80/100 »**, `metric: "ai_score", operator: "lt", threshold: 80, effect: "exclude"`, et le test vérifie `remuneratedEnergyContracts = 0`, `excludedEnergyContracts = 1`, `centreRemuneration = 0`. 79- **Le −4 € sous 85 y est aussi** : `functions/flexylead-app-pilotage-query.test.mjs:1684-1685`, `{ effect: "add", value: -4, conditions: [{ metric: "average_ai_score", operator: "lt", threshold: 85 }] }`. Repris dans `functions/flexylead-admin-mcp.test.mjs:217`. 80- **Le défaut livré du code est autre chose** : `defaultFormula` (`backend/flexylead-manage-production-economics.mjs:362-372`) ne contient **aucune** `sale_rules`, et son seul ajustement qualité est `quality-90` : `average_ai_score >= 90 → +5 €`. 81- `src/main.tsx:1402` (`score >= 80 → 'green'`) est un seuil **d'affichage**, pas de paiement — à ne pas confondre. 82 83**Rédaction admissible :** « Sous le seuil configuré — 80 sur 100 dans la règle « Vente non rémunérée sous 80/100 » que les tests du dépôt reproduisent —, la vente cesse d'être rémunérée. » 84 85**Conséquence chiffrée de #18 et #19, au barème que les tests reproduisent** : une correction manuelle sur un rapport `hop_selectra_universelle_v1` fait passer une vente de 100 % à 69,5 %, sous le seuil de 80 → **base 68 € ramenée à 0 €, soit −68 € par contrat corrigé**. Sur `selectra_compensation_carbone_v1`, 100 % → 14 % → même exclusion. En prime, la moyenne du centre chute, ce qui peut déclencher le −4 € par contrat du mois sous 85 de moyenne. 86 87## Ce que devient une vente sans note 88 89`saleRuleMatches` (`backend/flexylead-app-pilotage-remuneration-query.mjs:2416`) : `if (!Number.isFinite(Number(score))) return rule.missing === "match";`. Le champ `missing` est normalisé par `flexylead-manage-production-economics.mjs:329` avec **`"ignore"` par défaut**. Donc, sauf configuration contraire du centre : **une vente sans note n'est pas exclue — elle est payée au montant de base, c'est-à-dire comptée comme conforme.** Et elle est écartée de la moyenne du centre (`latestReportScoresForPeriod`, `:2580-2581`, saute les rapports sans score). Ce point n'est pas dans les quatre constats de la famille 3 ; il devrait l'être. 90 91## Commandes de recherche établissant les absences 92 93Exécutées depuis `code/flexylead-crm-0908/`, toutes en lecture seule : 94 95``` 96grep -c "diarize" functions/process-appel-db-only.mjs backend/process-appel-db-only.mjs \ 97 functions/process-appel-db-only.prod-active-before-prompt-fix.mjs → 0 0 0 98grep -cE "temperature|[^a-z]seed[^a-z]|top_p|top_k" functions/process-appel-db-only.mjs \ 99 backend/process-appel-db-only.mjs → 0 0 100grep -cE "corrected_at|corrected_by|ai_verdict|ai_score" functions/process-appel-db-only.mjs → 0 101grep -c "grilleScoring" src/dashboard/components/RapportViewer.tsx → 0 102grep -c "calculateEvaluationScore\|normalizeScoringMode\|base_score" \ 103 functions/process-appel-db-only.prod-active-before-prompt-fix.mjs → 0 104grep -rniE "diarize|diarization|speaker" backend/ functions/ src/ scripts/ workflows/ 105 → seules occurrences : backend/flexylead-transcribe-telephony-recording.mjs:207 (diarize:true) 106 et le softphone (src/lib/flexyleadSoftphone.ts, niveaux audio) — rien dans le moteur de notation 107grep -rnE "threshold: *80" backend/ → aucun résultat 108grep -rn "ai_analysis_model" backend/ functions/*.mjs → aucun écrivain 109``` 110 111## Ce que je n'ai pas pu établir 112 1131. **Quel moteur tourne réellement.** `backend/` est présenté comme l'archive des sources déployées (`README.md:11`), mais aucun manifeste de version ou d'empreinte n'accompagne les 73 fonctions d'origine — le seul fichier d'empreintes, `backend/SNAPSHOT-ADDITIONS-20260908.json`, ne couvre que **10** fonctions ajoutées le 08/09, et `process-appel-db-only` n'en fait pas partie. Le README avertit par ailleurs : « Git publication is not a production-readiness certification » et « The source directory includes current local, undeployed fixes » (`README.md:158`). **Le constat #11 ne peut pas être publié en l'état.** 1142. **Si les deux grilles en mode malus sont réellement chargées en base.** Les fichiers `config/*.json` n'ont **aucun chargeur dans le dépôt** : le seul semeur de grilles, `GRILLES_CONFIG` dans `flexylead-sync-local-google` (`functions/flexylead-sync-local-google.mjs:1953-1967 (`GRILLES_CONFIG`)`), ne déclare que `mint_energie_v1`, `ohm_v1`, `free_v1` et **ne porte ni `scoring_mode` ni `base_score`**. Les colonnes existent bien dans la table `grilles` (`functions/test-support/table-schema.json`). Les 69,5 et 14 sont donc **des chiffres de configuration livrée, pas des chiffres relevés en production**. À dire ou à faire confirmer par un relevé de la table `grilles`. 1153. **Les barèmes réellement en base** peuvent différer de `config/*.json`. Les 33 et 25 sont solides sur le fichier de configuration — dont le test ancre la provenance sur l'empreinte du classeur client (`mint-selectra-quality-v2-config.test.mjs:14`) — mais aucun relevé de la table `criteres` n'a été possible. 1164. **Le nombre de rapports déjà corrigés à la main** : il faudrait compter les lignes `evaluations` portant `corrected_at`, ce qui suppose un accès en lecture à la base. Non fait, régime strict respecté. 117 118## Contre-lecture — ce qui, dans le corpus, contredit mes conclusions 119 120- **Contre #11 (et donc contre le constat 2 tel que publié)** : l'hypothèse alternative « `backend/` a été produit depuis les sources de travail et non depuis la production » reste ouverte, et elle rendrait le constat vrai. Elle n'est **ni confirmée ni écartée**. Le nom `prod-active-before-prompt-fix` plaide en sens inverse : il désigne un état *antérieur*. 121- **Contre #14 / #18 / #19** : constats 2 et 3 sont **mutuellement exclusifs dans leurs effets**. Si le moteur additionnait déjà (constat 2), la note serait *déjà* de 69,5 et la correction manuelle ne la ferait pas chuter (constat 3 tomberait). Les deux ne peuvent pas être vrais du même dossier au même moment. Le document les publie côte à côte comme deux défauts cumulés : **c'est intenable et il faut choisir.** Ma lecture retient le constat 3, appuyé sur du code non ambigu et présent à l'identique dans les deux livraisons ; le constat 2 doit être reformulé en question ouverte adressée à Marco. 122- **Contre #3 et #5/#6** : les onze contrôles ne s'appliquent qu'aux codes M0xx et C0xx. Sur `free_v3`, `hop_selectra_universelle_v1` et `hop_selectra_universelle_v2`, l'exposition est **de zéro point** (calcul refait par code sur `config/*.json`). Le document a raison de dire « sur une grille… sur une autre » et ne doit pas être généralisé. 123- **Contre #4** : on pourrait objecter que le modèle, lui, distingue les locuteurs au sens et corrige la sonde. Écarté : `enforceParsedBusinessRules` s'exécute **après** `parseRapport` et **écrase** le verdict du modèle (`e.verdict = 'OK'; e.score = Number(e.bareme||0)`, `functions/process-appel-db-only.mjs:2489-2491`). La sonde a le dernier mot. 124- **Contre #25** : on pourrait objecter que l'absence de `temperature` laisse un défaut fournisseur qui pourrait être bas. Non écarté formellement — mais le levier reste le même : rien n'est fixé côté FLEXYLEAD, et rien n'est enregistré pour le vérifier après coup (#26). 125