Preuve · rapport

famille3-notation-vs-deploye-2026-09-14.md

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

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

1# Famille 3 « La notation qualité » — reprise contre le déployé du 14/09/2026
2
3**Auditeur :** `audit-qualite-ia` · 14/09/2026 · **lecture seule**, aucun appel réseau, aucune clé, aucun code applicatif exécuté.
4**Corpus :** `code/flexylead-crm-2026-09-14/` (HEAD `ccbbaa07`, « Sync production Code and Functions snapshots », 14/09 08:46:08 +0200) · `code/flexylead-crm-0908/` (`730a5e7`) · `analyse/preuves-notation-2026-09-09.md` · `analyse/liste-23-constats-reconstituee-2026-09-13.md`.
5
6Tous les chemins ci-dessous sont relatifs à `code/flexylead-crm-2026-09-14/` sauf mention `0908`.
7
8---
9
10## 1. Périmètre traité et ce qui en est exclu
11
12**Traité :** les 4 constats de la famille 3 (10 à 13) rejoués contre le déployé ; le diff du moteur ; la grille `lead_telecom_v3` ; le chemin de correction manuelle ; la chaîne de rémunération qui consomme la note.
13
14**Exclu :** les constats des familles 1, 2 et 4 ; la base de production (aucun accès — c'est la limite déterminante de ce rapport, voir §7) ; les 14 fichiers audio ; le constat 19, signalé au §8 sans être instruit.
15
16---
17
18## 2. Le socle : la garantie du 09/09 est cassée, et dans un seul sens
19
20Le rapport du 09/09 reposait sur une parité (`preuves-notation-2026-09-09.md:17-21`) : le moteur était identique entre les deux livraisons **et** entre `functions/` et `backend/`, md5 `9d7b7dc2aadd231836f04e480eecaa22`.
21
22Relevé au 14/09 :
23
24| copie | md5 | lignes / octets | évolution |
25|---|---|---|---|
26| `functions/process-appel-db-only.mjs` | `9d7b7dc2aadd231836f04e480eecaa22` | 3 962 / 221 296 | **inchangée** depuis le 08/09 |
27| `backend/process-appel-db-only.mjs` | `dae136928080a0ceccd049e14a84e754` | 3 972 / 222 534 | **modifiée** par `ccbbaa07` |
28| `functions/process-appel-db-only.prod-active-before-prompt-fix.mjs` | `db2bf039db3e33e766fb4331b66acf87` | 2 338 / 123 894 | **inchangée** depuis le 08/09 |
29
30`FAIT` — La source de travail n'a pas bougé ; c'est la copie archivée comme déployée qui a avancé. **Le déployé est en avance sur la source**, ce qui est l'inverse du sens attendu d'un dépôt.
31
32`FAIT` — Le diff du moteur tient en **deux blocs et rien d'autre** (`diff -u` entre les deux `backend/`) :
33
341. **+10 lignes** de consignes de prompt insérées dans `renderGrilleText`, `backend/process-appel-db-only.mjs:1221-1231`, conditionnées à `grille?.local_id === 'lead_telecom_v3'`.
352. **Deux substitutions de grille** dans les heuristiques de chemin : `free_v3` → `lead_telecom_v3` en `:3306` et `:3309`.
36
37C'est ce décalage de +10 lignes qui explique tous les déplacements de numéros de ligne entre le rapport du 09/09 et celui-ci (ex. la sonde impayés : `:1005` → `:1002`… en réalité `-3`, voir §4 ; l'agrégation : `:1244` → `:1254`). **Aucune autre ligne du moteur n'a changé.**
38
39`FAIT` — La phrase du README qui fondait la lecture « `backend/` = déployé » a **disparu**. Au 08/09, `0908/README.md:11` : « `backend/` contains deployed-source snapshots; these are not the editing source of truth » ; au 14/09 le README est passé de 162 à 111 lignes et `grep` sur `deployed-source|not the editing source|snapshot` ne ramène **rien**. Il ne reste que le message de commit — une déclaration, qui se situe au dernier rang de la hiérarchie de preuve de `CLAUDE.md`.
40
41`FAIT` — La qualité de « déployé » de `backend/` n'est pas homogène : **61 fichiers sur 101** portent l'adaptateur d'autorisation `__authParityEvent`, **0 sur 147** dans `functions/`. Mais `backend/process-appel-db-only.mjs`, `backend/process-appel.mjs` et `backend/quality-partner-gateway.mjs` en portent **zéro**. Le marqueur qui distingue le déployé de la source est donc absent précisément des fichiers de la famille 3.
42
43---
44
45## 3. La grille `lead_telecom_v3` — le fait nouveau le plus lourd
46
47`FAIT` — `lead_telecom_v3` **n'existe nulle part ailleurs que dans le moteur déployé**. `grep -rn` sur tout le dépôt (hors `.git`) : **5 occurrences, toutes dans `backend/process-appel-db-only.mjs`** (`:1221`, `:3306`, `:3307`, `:3309`, `:3310`). Zéro dans `config/`, zéro dans les semeurs, zéro dans les tests, zéro dans `src/`, **zéro dans le corpus du 08/09**.
48
49`FAIT` — Ce qu'elle remplace est, lui, complètement défini : `free_v3` possède `config/free_quality_v3.json` (**25 critères, somme des barèmes = 100,0**), un test qui l'ancre (`functions/free-quality-v3-config.test.mjs:9` « Free v3 is a complete, stable 100-point rubric », `:25` somme = 100) et **9 offres** qui pointent vers elle (`functions/flexylead-sync-local-google.mjs:488-497`).
50
51`FAIT` — L'heuristique de chemin **prime sur la chaîne vente→offre**. Le commentaire du code le dit (`backend/process-appel-db-only.mjs:3291-3293` : « runs BEFORE the vente→offre chain so unambiguous folder signals win over a potentially-stale vente link ») et la structure le confirme : `if (!grilleId) { … selectEvaluationOffer … }` en `:3313-3317`. Donc même si l'offre de la vente déclare `free_v3`, **le dossier Drive impose `lead_telecom_v3`**.
52
53`FAIT` — Neuf familles de dossiers Drive sont concernées, rattachables à leurs centres par `functions/flexylead-sync-local-google.mjs:452-464` : **Reda** (`/Enregistrements de Reda`, `/Enregistrements Protected - Février - Mars`), **ATM**, **GRF Connect** (`/Enregistrements Free`), **CaptoPhone**, **Nabadji X**, **TINGIS**, **MGK**, **Mon Assistant Perso**.
54
55`FAIT` — **Aucun semeur du dépôt ne peut créer cette grille.** `GRILLES_CONFIG` ne déclare que `mint_energie_v1`, `ohm_v1`, `free_v1` — dans le semeur déployé (`backend/flexylead-sync.mjs:1318-1332`) comme dans les deux autres copies (`backend/flexylead-sync-local-google.mjs`, `functions/flexylead-sync-local-google.mjs:1953-1967`). `grep -c "lead_telecom|telecom_v3"` sur ces trois fichiers → **0, 0, 0**. Les `config/*.json` restent **sans chargeur** : les seuls fichiers qui les lisent sont quatre fichiers de test (`free-quality-v3-config.test.mjs`, `hop-selectra-quality-config.test.mjs`, `mint-selectra-quality-v2-config.test.mjs`, `process-appel-db-only.test.mjs`). Ce point du 09/09 (`preuves-notation:114`) est donc **toujours vrai au 14/09**.
56
57### Deux explications concurrentes, aucune tranchée par le code
58
59- **(H-A) La grille a été créée directement en base par Marco.** Alors le sens de la synchro est une **re-notation délibérée** de tout le périmètre Free/lead télécom sur un référentiel neuf à **sept critères** (le prompt dit « les sept critères fournis », `:1223`, codes `LT001`…`LT007` d'après `:1225`), là où `free_v3` en comptait 25. Conséquence : les notes rendues avant et après ne sont **pas comparables**, et elles alimentent la même moyenne `average_ai_score`.
60- **(H-B) La grille n'existe pas en base.** Alors `grilleId` est renseigné mais `criteres.length === 0`, et le moteur prend la branche `if (!grilleId || !criteres.length)` (`:3341-3369`) : il marque l'appel `no_grille`, **ne crée aucun rapport** et retourne `skipped: 'no_grille'`. **Tout le périmètre Free cesse silencieusement d'être noté.**
61
62`HYPOTHÈSE` — Aucune des deux n'est écartable sans un relevé de la table `grilles`. Le fait qui penche vers H-A : on ne modifie pas un prompt pour une grille inexistante. Le fait qui penche vers H-B : rien, dans aucune des trois copies du dépôt, ne crée cette grille, et `free_v3` elle-même n'est semée par rien — donc l'existence en base ne se déduit d'aucun automatisme du dépôt.
63
64**C'est la question à poser à Marco en priorité, avant toute autre de cette famille.**
65
66---
67
68## 4. Statut des quatre constats
69
70### Constat 10 — « Le texte de l'appel ne dit pas qui parle, et onze contrôles y cherchent des phrases » → **INTACT**
71
72`FAIT` — Sur le moteur déployé : `grep -c` sur `diarize` → **0**, `diarization` → **0**, `speaker` → **0**. La transcription est toujours demandée sans séparation des interlocuteurs.
73
74`FAIT` — Les contrôles qui forcent un verdict depuis le texte sont toujours là, avec le même effet. La sonde impayés est inchangée au caractère près (`backend/process-appel-db-only.mjs:1002-1009`), y compris l'alternance qui capte l'énoncé du client :
75
76```
77/(?:avez[- ]?vous|vous avez|pas|aucun|aucune)[^.?!]{0,100}(?:impaye|retard de paiement|dette)/i
78```
79
80et son effet en `:2498-2502` : `e.verdict = 'OK'; e.score = Number(e.bareme || 0);`. Le client qui dit « je n'ai pas d'impayé » crédite toujours le commercial.
81
82`FAIT` — Le dénombrement est à préciser par rapport au 09/09 : le bloc `enforceParsedBusinessRules` compte **17 branches par code de critère** (`:2441` HA003, `:2447` C001, `:2452` C015, `:2458` C014, `:2463` C010, `:2468` C011, `:2473` C007, `:2478` M003, `:2483` et `:2491` M004, `:2498` M013, `:2503` M014, `:2511` F001, `:2526` M001, `:2557` M017, `:2583` M018, `:2589` M011), dont onze s'appuient sur une recherche de tournure dans le transcript. Le chiffre « onze » du document reste défendable **à condition de dire « onze contrôles cherchent des phrases »** et non « onze contrôles » tout court.
83
84`Contre-lecture` — On pourrait objecter que le modèle distingue les locuteurs au sens et corrige la sonde. **Écarté, et pour la même raison qu'au 09/09** : `enforceParsedBusinessRules` s'exécute après `parseRapport` et écrase le verdict du modèle. La sonde a le dernier mot.
85
86### Constat 11 — « Une grille en mode malus est additionnée au lieu d'être déduite » → **TRANCHÉ SUR LE MOTEUR, DÉPLACÉ SUR LE FOND**
87
88C'était le point bloquant. Il peut être débloqué, mais pas dans le sens espéré.
89
90`FAIT` — **Le moteur déployé déduit.** `backend/process-appel-db-only.mjs:1254-1266` :
91
92```js
93function calculateEvaluationScore(evaluations, criteres, grille = null) {
94  const earned = evaluations.reduce((sum, e) => sum + (Number(e?.score) || 0), 0);
95  const baremeTotal = criteres.reduce((sum, c) => sum + (Number(c?.bareme) || 0), 0);
96  if (normalizeScoringMode(grille) !== 'deduction') {
97    return { score_total: earned, score_max: baremeTotal };
98  }
99  const baseScore = gridBaseScore(grille);
100  const appliedPenalty = Math.max(0, baremeTotal - earned);
101  return { score_total: Math.max(0, Math.min(baseScore, baseScore - appliedPenalty)), score_max: baseScore };
102}
103```
104
105Machinerie présente et **strictement identique** dans les deux arbres : `normalizeScoringMode` ×5, `calculateEvaluationScore` ×3, `gridBaseScore` ×3, `deduction` ×5, dans `backend/` comme dans `functions/`. Le prompt système bascule lui aussi (`:1851-1853`). L'affirmation publiée « la version réellement en service ne connaît ni la note de départ, ni le retrait — elle additionne » est **fausse pour les deux copies principales**.
106
107`FAIT` — **La troisième copie est inchangée**, md5 identique au 08/09, `grep -c` = **0** sur `normalizeScoringMode`, `calculateEvaluationScore`, `gridBaseScore`, `base_score`, `scoring_mode`, `deduction`. Elle a été ajoutée par `730a5e7` (08/09) et n'a été touchée par aucune des deux synchros.
108
109`SUGGÉRÉ` — **La synchro permet de la traiter comme un artefact historique, sans le prouver.** L'argument nouveau est bon mais indirect : le fichier se nomme `prod-active-before-prompt-fix`, et `ccbbaa07` est précisément une modification de prompt (`:1221-1231`). La chronologie est donc cohérente avec « état antérieur au correctif de prompt ». Elle ne l'établit pas : rien ne date le fichier, il est resté dans `functions/` au lieu d'être retiré, et `backend/process-appel-db-only.mjs` ne porte pas l'adaptateur d'autorisation qui marque les 61 autres fichiers déployés. **Niveau maximal atteignable sur pièces : `SUGGÉRÉ`.**
110
111`FAIT` — **Le vrai défaut est ailleurs, et il est structurel : la branche « malus » est inatteignable pour toute grille que le dépôt sait créer.**
112- `normalizeScoringMode` (`:1243-1247`) retourne `'additive'` dès que `grille.scoring_mode` est absent, vide ou inconnu ; `gridBaseScore` (`:1249-1252`) retombe sur 100.
113- **Aucun semeur n'écrit jamais `scoring_mode` ni `base_score`** : `grep -c "scoring_mode|base_score"` → **0** sur `backend/flexylead-sync.mjs`, **0** sur `backend/flexylead-sync-local-google.mjs`, **0** sur `functions/flexylead-sync-local-google.mjs`. Les colonnes existent pourtant bien dans la table (`functions/test-support/table-schema.json`, table `grilles` : `local_id`, `fournisseur`, `version`, `active`, `applies_to_offres`, **`scoring_mode`**, **`base_score`**).
114- Les deux seules grilles en mode malus (`hop_selectra_universelle_v1`, malus max 69,5 ; `selectra_compensation_carbone_v1`, malus max 14) n'existent que dans `config/hop_selectra_quality_v1.json` et `config/hop_selectra_quality_v2.json:75-76`, fichiers **identiques au 08/09** (md5 inchangés) et **lus par les seuls tests**.
115
116→ **Le mode malus est une fonctionnalité écrite, testée, et jamais semée.** C'est une formulation plus forte et mieux étayée que celle du document publié, et elle sort de la contradiction relevée par `relecture-coherence-2026-09-09.md:11`.
117
118`FAIT` — **Sur la phrase « Conserver le calcul additif sur 100 points » : elle déplace le problème, elle ne le règle pas.** Cette consigne (`:1230`) est adressée **au modèle**, dans le texte du prompt. Le mode de calcul du serveur est décidé ailleurs, par `normalizeScoringMode(grille)` qui lit la **base**, pas le prompt. Les deux peuvent donc diverger : si `lead_telecom_v3` portait `scoring_mode='deduction'` en base, le prompt dirait « additif sur 100 » pendant que le serveur déduirait — exactement la classe de défaut que le constat 11 décrit. En pratique le risque est faible, puisque le défaut est `'additive'` ; mais la phrase **ne constitue aucune garantie**, et elle ne doit pas être citée comme un correctif.
119
120**Verdict : le constat 11 doit être réécrit une troisième fois.** Titre proposé : « **Le mode “malus” des grilles Selectra n'est appliqué par rien : aucun semeur ne l'écrit en base** ». La question ouverte à Marco change elle aussi : ce n'est plus « quel moteur tourne ? » mais « **les grilles `hop_selectra_universelle_v1` et `selectra_compensation_carbone_v1` existent-elles en base, et avec quel `scoring_mode` ?** ».
121
122### Constat 12 — « Corriger une note à la main la fait tomber sous le seuil de paiement » → **PARTIELLEMENT CORRIGÉ CÔTÉ ÉCRAN, NON VÉRIFIABLE CÔTÉ SERVEUR**
123
124`FAIT` — **Le mécanisme publié n'est plus sur le fil.** Au 08/09, `correctRapport` écrivait directement la somme brute :
125
126```ts
127// 0908/src/dashboard/api/client.ts:7210
128await adminDataMutation('update', 'rapports', { local_id: rapportLocalId, fields: {
129    evaluations: fields.evaluations, score_total: fields.score_total, modified_time: ... } });
130```
131
132Au 14/09 (`src/dashboard/api/client.ts:7039-7050`), elle n'envoie **plus de score du tout** : elle relit le rapport (`rapport_detail`), calcule la liste des seules lignes changées, exige un commentaire par correction, et poste `{action:'rapport_correct', rapport_id, expected_modified_time, corrections}`. Le `score_total` que `RapportViewer.tsx:125` calcule encore par somme brute lui est passé mais **n'est plus lu** : c'est devenu une valeur morte. L'écran affiche désormais le score **renvoyé par le serveur** (`RapportViewer.tsx:128-129`, `setLocalScore(saved.score_total)`).
133
134`FAIT` — **Mais aucun serveur livré n'implémente `rapport_correct`.** La route `/v1/quality/query` est servie par `flexylead-app-quality-query`, dont le manifeste désigne le fichier `functions/quality-partner-gateway.mjs` (`scripts/application-api-manifest.mjs:32` et `:123`). Or :
135- `functions/quality-partner-gateway.mjs:74-88` — liste d'actions close par `throw new Error("Unsupported partner gateway action.")` ; **pas de `rapport_correct`** ; `grep -ci "correct"` → **0**.
136- `backend/quality-partner-gateway.mjs:70-81` — idem, `grep -ci "correct"` → **0**.
137- `backend/flexylead-app-quality-query.mjs:138-151` — idem, `grep -ci "correct"` → **0**.
138- `grep -rln "rapport_correct" backend/ functions/` → **aucun fichier**. Les seules occurrences du dépôt sont le README, deux écrans et des tests à serveur simulé.
139
140`FAIT` — Le même écart frappe le **panneau « Analyse approfondie »** apparu dans `src/components/QualityReviewPanel.tsx` : il poste `review_list`, `review_request`, `review_apply`, `review_keep` (`:11`, `:17`) et son bouton « Appliquer » remplace la note (`:40`, libellé « Nouvelle note appliquée » `:6`). **Aucune de ces quatre actions n'existe dans `backend/` ni `functions/`**, et la table qui porterait ces revues est **absente des 54 tables** du schéma livré.
141
142`FAIT` — Ce qui n'a pas bougé du constat 12 :
143- `grilleScoringSummary` est toujours appelée **zéro fois** dans `src/dashboard/components/RapportViewer.tsx` (`grep -c` → 0), alors que `src/dashboard/lib/grilleScoring.ts:3` existe et sait faire le malus. Ses seuls appelants restent `src/dashboard/pages/Criteres.tsx:10,128` et son test. **Les deux fichiers sont inchangés au bit près** (md5 identiques à ceux du 08/09).
144- `RapportViewer` reste bien câblé, par deux écrans : `src/dashboard/pages/Appels.tsx:7,332` et `src/dashboard/pages/Rapports.tsx:6,231`. Ce n'est pas du code mort.
145- La politique d'écriture admin autorise toujours `score_total` sans autoriser `score_max` : `backend/flexylead-admin-data-mutation.mjs:70`, `rapports: policy(['evaluations', 'score_total', 'modified_time', 'status', 'updated_at_iso'])`. Le dénominateur reste donc figé à 100 quoi qu'il arrive.
146
147`HYPOTHÈSE` vs `HYPOTHÈSE` — Deux lectures, non départageables ici :
148- **(a) Le serveur existe en production mais n'est pas dans la livraison.** Plaide pour : `backend/flexylead-app-quality-query.mjs` est déployé-seul (aucune contrepartie dans `functions/`) et a grossi de 110 lignes ; le README décrit le mécanisme comme opérationnel (`README.md:108-111`).
149- **(b) Il n'est réellement pas déployé**, et alors **toute correction manuelle échoue** avec « Unsupported partner gateway action », de même que toute seconde analyse.
150
151**Dans les deux cas, le constat 12 tel que publié n'est plus démontrable sur le code livré**, et son remède (« faire calculer la correction comme le moteur ») est soit déjà fait, soit inapplicable. À noter pour la suite : la phrase du README qui décrit le nouveau calcul — « recomputes the score from the stored criterion scale » — parle **d'une somme de barèmes**, sans un mot de `scoring_mode` ni de `base_score`. Si le serveur existe et fait ce que le README dit, **il reproduit le défaut additif** sur une grille en mode malus.
152
153**Conséquence sur l'incompatibilité signalée le 09/09** (`relecture-coherence:13` : les constats 11 et 12 ne pouvaient pas être publiés ensemble) : elle **tombe**. Le constat 11 ne dit plus que le moteur additionne, et le constat 12 ne repose plus sur le même mécanisme. Les deux peuvent coexister une fois réécrits.
154
155### Constat 13 — « Le même appel peut recevoir deux notes différentes » → **INTACT, ET AGGRAVÉ**
156
157`FAIT` — Le levier est inchangé : `grep -c` sur `temperature`, `seed`, `top_p`, `top_k` → **0** sur `backend/process-appel-db-only.mjs`, **0** sur `backend/process-appel.mjs`. Aucune température, aucune graine. Le non-déterminisme est **subi, pas configuré**.
158
159`FAIT` — Rien n'est conservé pour départager : `grep -cE "corrected_at|corrected_by|ai_verdict|ai_score"` sur le moteur déployé → **0**. La table `rapports` compte **17 colonnes** (`functions/test-support/table-schema.json`) et **aucune ne porte le modèle ni le prompt**. Les colonnes `ai_analysis_model` / `ai_analysis_version` de la table `appels` n'ont toujours pour seuls écrivains que trois semeurs de démonstration (`scripts/seed-local-heavy-production.mjs`, `scripts/seed-staging-lucas-current-day.mjs`, `scripts/seed-staging-mk0819-current-day.mjs`).
160
161`FAIT` — Le départage mécanique est confirmé et doublement sourcé : `latestReportScoresForPeriod` (`backend/flexylead-app-pilotage-remuneration-query.mjs:2676-2695`) retient `if (!current || modified >= current.modified)` sur la clé `saleIds[0] || appel_id || id` ; et le SQL agrégé classe par `ROW_NUMBER() OVER (PARTITION BY centre_appel_id, COALESCE(sale_id, appel_id, id) ORDER BY modified_time DESC, …)` (`:4244-4260`). **La plus récente gagne**, sans qu'on puisse dire si c'est la bonne.
162
163`FAIT` — **AGGRAVATION : un second moteur de notation est apparu dans le déployé.** `backend/process-appel.mjs` (1 609 lignes, 73 186 o), ajouté par `ccbbaa07`, **absent de `functions/`**. Il écrit dans la **même table `rapports`** (`:1584`, `:1587`) et diffère du moteur principal sur les deux points qui comptent :
164
165| | `process-appel-db-only.mjs` (déployé) | `process-appel.mjs` (déployé, nouveau) |
166|---|---|---|
167| agrégation | `calculateEvaluationScore` avec `scoring_mode` / `base_score` (`:1254-1266`) | **somme brute uniquement** : `score_total = Σ scores`, `score_max = Σ barèmes` (`:1244-1246`) ; `grep -c` sur `normalizeScoringMode`, `calculateEvaluationScore`, `scoring_mode`, `base_score` → **0, 0, 0, 0** |
168| clé de dédoublonnage | `exactRapportFirstLookup` sur `appel_id` **ou** `appel_ids` **ou** `vente_ids`, en excluant les grilles additionnelles (`:3560-3572`) | `WHERE appel_id = ? ORDER BY id DESC LIMIT 1` (`:1564-1566`) |
169
170`SUGGÉRÉ` — **Il semble dormant, mais rien ne le garantit.** `workflows/process-appel.yaml` est **identique** à celui du 08/09 (`diff` vide) et déclenche la fonction nommée `process-appel-db-only` (`:12`) ; le manifeste ne déclare que `process-appel-db-only` (`scripts/application-api-manifest.mjs:76`). Donc aucun câblage connu. Mais le fichier est dans `backend/`, c'est-à-dire l'ensemble que le commit désigne comme les instantanés de production. **Deux moteurs qui écrivent la même table avec deux clés de dédoublonnage différentes et deux agrégations différentes, c'est exactement le mécanisme du constat 13** — il faut faire confirmer son état à Marco.
171
172`FAIT` — Ce second moteur est aussi la **source littérale** d'un défaut relevé sur pièces : « **Non applicable | 100 % du bareme (crédit accordé par défaut)** » (`backend/process-appel.mjs:87`), « le total reste toujours /100 » (`:89`, `:175`). La même règle est dans le moteur principal (`backend/process-appel-db-only.mjs:54`, `:1936`) et dans son code : `reconcileEvalScore` (`:2069-2088`) donne le **barème plein** à `OK` comme à `Non applicable`.
173
174`FAIT` — Consigné sans recommandation, conformément à l'arbitrage du 02/09 : `backend/process-appel.mjs:25` porte une clé d'API en clair (`apiKey: 'ak_cpsw-…'`) et `:32` un identifiant d'utilisateur ; la même clé figure dans `backend/flexylead-sync.mjs`. C'est un **fait nouveau du 14/09**, pertinent pour la régénération des secrets au changement d'hébergeur.
175
176---
177
178## 5. Traduction en euros, au barème en vigueur
179
180Le barème financier est **inchangé** entre les deux livraisons : `functions/flexylead-app-pilotage-query.test.mjs` a le **même md5** (`22bb0d8e7ee8e9cd624ed502965f6d49`), et porte toujours la règle « Vente non rémunérée sous 80/100 » (`:1784-1785`, `metric: "ai_score", operator: "lt", threshold: 80, effect: "exclude", missing: "ignore"`) et le −4 € sous 85 de moyenne (`:1684-1685`, `:1736-1737`). Le défaut livré reste `quality-90` : +5 € au-delà de 90 (`backend/flexylead-manage-production-economics.mjs:420`).
181
182**Que devient une vente sans note :** `saleRuleMatches` (`backend/flexylead-app-pilotage-remuneration-query.mjs:2511-2513`) — `if (!Number.isFinite(Number(score))) return rule.missing === "match";` — et `missing` est normalisé à **`"ignore"` par défaut** (`backend/flexylead-manage-production-economics.mjs:380`). Donc **une vente sans note n'est pas exclue : elle est payée au montant de base, comptée comme conforme.** Et elle est écartée de la moyenne du centre (`:2679-2681`, les rapports sans score fini sont sautés).
183
184**Chiffrage du scénario H-B** (la grille `lead_telecom_v3` n'existe pas en base), sur les huit centres du périmètre Free listés au §3 :
185
1861. **Par vente : erreur en faveur du centre.** Chaque vente Free cesse d'être notée → elle échappe à l'exclusion « sous 80/100 » → elle est payée alors qu'elle aurait pu ne pas l'être. Au barème de référence du 09/09 (base 68 €), **+68 € par vente qui aurait dû être exclue**.
1872. **Par mois : erreur contre le centre, et elle est brutale.** `average_ai_score` vaut **0** quand aucun rapport n'est noté sur la période (`:2664`, `scores.length ? moyenne : 0`). Or `0 < 85` → la règle −4 € **s'applique à chaque contrat du mois** ; et `0 < 90` → le bonus `quality-90` de +5 € est perdu. **Soit −9 € par contrat du mois** pour un centre dont la formule porte les deux règles, contre un centre qui tenait 90.
188
189Les deux effets vont **en sens opposés**, ce qui rend le solde net imprévisible sans relevé de la base — et c'est précisément ce qui rend la situation inacceptable pour un calcul de paie : **l'erreur n'est pas bornée, et son signe dépend du volume**.
190
191Sous le scénario H-A, le chiffrage est différent mais non nul : les notes rendues avant et après la bascule reposent sur **deux référentiels distincts** (25 critères contre 7), et elles sont **moyennées ensemble** par `latestReportScoresForPeriod` sans aucune normalisation autre que `score_total / score_max * 100` (`:2694`). Un mois à cheval sur le 14/09 mélange donc deux échelles dans la moyenne qui déclenche le palier des 85.
192
193---
194
195## 6. Réponses aux sept questions permanentes, sur le déployé
196
1971. **Fonction d'agrégation** : `calculateEvaluationScore`, `backend/process-appel-db-only.mjs:1254-1266`. La valeur par défaut d'un verdict sans score est donnée par `reconcileEvalScore` (`:2069-2088`) : `OK` et **`Non applicable` → barème plein** ; `OK partiel` → 50 % ; `KO` → 0 ; verdict non reconnu sans score → 0. **Elle est la même pour tous les critères à ce niveau**, mais **17 branches par code** la surchargent ensuite (`:2441-2600`), et une d'entre elles annule la note entière (`:2736-2739`, C007 sur `conso_pilote_v2` → `score_total = 0`).
1982. **Prompt** : l'appel **entier** pour la cible (`buildEvalUser`, `:1941`) ; seuls les transcripts *frères* sont tronqués à 16 000 caractères (`:1847`). Grille **complète**, une ligne par critère avec barème et marque `[CRITIQUE]` (`:1233-1239`). **`critique` est transmis ; `applicable` ne l'est pas** — `renderGrilleText` ne l'écrit jamais, alors que la colonne existe (`table-schema.json`, table `criteres`). Inchangé depuis le 09/09.
1993. **Paramètres du modèle** : aucun. Zéro `temperature`, `seed`, `top_p`, `top_k`. **Non-déterminisme subi.**
2004. **Regex de rattachement** : elle n'est pas dans le moteur. Elle est par centre, dans `functions/flexylead-sync-local-google.mjs:452-464` (`call_id_patterns`), et diffère d'un centre à l'autre : Reda `(?<!\d)(\d{7,9})(?!\d)`, Nabadji `(?<=-)(\d{7,8})(?=-)`, MGK `FBX(\d{7,8})|fbx(\d{7,8})|(?<!\d)(\d{7,8})(?!\d)`. Côté moteur, c'est le **chemin du dossier** qui choisit la grille. **Le comportement en correspondance multiple ou nulle n'a pas été tracé ici** — hors du périmètre confié, à instruire séparément.
2015. **Note qui fait foi** : **la plus récente** par `modified_time`, clé `vente_ids[0] || appel_id || id` (`:2676-2695` et `:4244-4260`). C'est bien celle que consomme le calcul de rémunération.
2026. **Recalcul rétroactif** : le moteur réécrit le rapport de bout en bout et **ne lit aucun marqueur de correction** (`grep` sur `corrected_at|corrected_by` → 0). **Un rapport corrigé à la main ne survit pas au recalcul suivant.** Inchangé.
2037. **Vente sans note** : **comptée comme conforme et payée** (voir §5).
204
205---
206
207## 7. Ce que je n'ai pas pu établir, et pourquoi
208
2091. **Si `lead_telecom_v3` existe en base, et avec quels critères et quel `scoring_mode`.** C'est la question qui commande tout le §3 et tout le §5. Aucun accès à la table `grilles` — régime lecture seule respecté.
2102. **Si le serveur qui traite `rapport_correct` et `review_*` existe en production.** Il est absent des deux arbres livrés ; le README affirme qu'il fonctionne. Une déclaration ne vaut pas preuve.
2113. **Si `backend/process-appel.mjs` est invoqué.** Aucun câblage dans le dépôt, mais le dépôt ne prouve pas l'inventaire réel des fonctions déployées.
2124. **Laquelle des trois copies du moteur tourne.** Le faisceau s'est nettement renforcé en faveur de `backend/`, mais la disparition de la phrase du README et l'absence d'adaptateur d'autorisation sur ce fichier précis interdisent de passer à `FAIT`.
2135. **Le nombre de rapports déjà rendus par grille**, donc l'assiette réelle du chiffrage. Suppose un accès en lecture à `rapports`.
214
215---
216
217## 8. Signalements hors périmètre
218
219- **Constat 19 (« Le consentement du client n'est rangé nulle part »)** — à porter à `audit-donnees` et `audit-contradicteur`. Le prompt déployé érige désormais le **consentement RGPD en critère bloquant** (`backend/process-appel-db-only.mjs:1225`, LT002, avec obligation de le signaler même si le score est élevé). La chaîne qualité se met donc à **produire un jugement sur le consentement** alors que, d'après le constat 19, aucune colonne ne le stocke. Le verdict existe, la donnée non : ces deux constats doivent être relus ensemble.
220- **Secrets** — `backend/process-appel.mjs:25,32` et `backend/flexylead-sync.mjs` : clé Composio et identifiant utilisateur en clair, **nouveaux au 14/09**. Consigné factuellement, sans relance, conformément à l'arbitrage du 02/09.
221
222---
223
224## 9. Contre-lecture — ce qui pourrait faire tomber ces conclusions
225
226- **Contre le §3 :** si `lead_telecom_v3` est en base avec sept critères cohérents, la synchro est une amélioration délibérée et non un incident. Je n'ai pas écarté cette lecture ; je la donne en premier (H-A). Ce qui resterait vrai dans tous les cas : la grille précédente valait 100 points sur 25 critères, la nouvelle est annoncée à 7, et les deux échelles se mélangent dans la moyenne qui déclenche le palier des 85.
227- **Contre le §4 / constat 12 :** j'affirme qu'aucun serveur ne sert `rapport_correct`. Si Marco livre le fichier manquant, la conclusion bascule de « non vérifiable » à « corrigé ». J'ai vérifié dans les trois passerelles candidates et par `grep` global ; je n'ai pas pu vérifier l'inventaire réel des fonctions déployées.
228- **Contre le §4 / constat 13 :** je qualifie `process-appel.mjs` de « dormant » au niveau `SUGGÉRÉ` uniquement. Si le fichier est dans `backend/` parce qu'il **est** déployé — ce que le message de commit affirme — alors le constat 13 doit être requalifié en **aggravation établie**, et non en risque.
229- **Contre l'ensemble :** toute cette famille repose sur l'équation « `backend/` = production ». Cette équation était appuyée par le README au 08/09 ; **elle ne l'est plus au 14/09**. Si elle est fausse, les quatre constats changent de base de preuve — et c'est `functions/`, inchangé depuis le 08/09, qu'il faudrait lire comme la production.
230