1# Famille 4 « Le travail, les entrées, les traces » — les 10 constats (14 à 23) contre le déployé du 14/09 2 3**Date :** 14/09/2026 · **Régime :** lecture seule, aucun appel réseau, aucune clé. Écriture dans `analyse/` uniquement. 4**Poste :** `audit-routage`. **Charte :** `.claude/agents/_CHARTE.md`. 5**Révision du même jour :** la section du **constat 16** a été reprise et durcie après une remontée du poste `audit-donnees` (voir § 1, constat 16). Le verdict est passé de « corrigé sur le cœur » à « **partiellement corrigé — deux compteurs sur quatre** ». 6 7## Conventions 8 9| Sigle | Chemin | Nature | 10|---|---|---| 11| **N** | `code/flexylead-crm-2026-09-14/backend/` (`ccbbaa07`) | instantané dit « déployé » du 14/09, **101 entrées** | 12| **D2** | `code/flexylead-crm-0908/backend/` (`730a5e7`) | instantané dit « déployé » du 08/09, **84 entrées** | 13| **S** | `code/flexylead-crm-2026-09-14/functions/` | sources de travail du 14/09, 149 entrées | 14| **S2** | `code/flexylead-crm-0908/functions/` | sources de travail du 08/09 | 15 16Racine de tous les chemins : `/Users/gregorydelaloeuf/Documents/scalewayxflexylead/`. 17Base de comparaison : `analyse/liste-23-constats-reconstituee-2026-09-13.md` (famille 4) et `analyse/preuves-travail-2026-09-09.md`. 18 19--- 20 21# 0. Ce qui commande la lecture de tout le reste 22 23## 0.1 La réserve sur le statut de `backend/` est plus grave qu'annoncé — `FAIT` 24 25Le README est passé de **162 à 111 lignes**. La phrase « `backend/` contains deployed-source snapshots; these are not the editing source of truth » (`code/flexylead-crm-0908/README.md:11`) a disparu, **ainsi que toute mention de `backend/`** : `grep -n "backend\|snapshot\|deployed-source"` sur `code/flexylead-crm-2026-09-14/README.md` → **zéro résultat**. La ligne `:156` du 08/09 (« historical deployed snapshots ») a disparu aussi. 26 27Le nouveau README ne parle plus que de l'application, des rôles et des routes. **Rien, dans la livraison du 14/09, ne dit que `backend/` est le déployé.** Le seul support est le message de commit « Sync production Code and Functions snapshots » — une déclaration, au sens de la hiérarchie de la charte (production réelle > code déployé > configuration > documentation > **déclarations**). 28 29**Aggravant, non signalé dans le cadrage :** `CANONICAL_SOURCE.json` est **inchangé** entre les deux livraisons et porte toujours `"deployment_status": "staging-candidate"` et `"staging_domain": "commerciaux.test.flexylead.com"` (`code/flexylead-crm-2026-09-14/CANONICAL_SOURCE.json:9-11`). Le seul fichier du dépôt qui qualifie explicitement un état de déploiement dit **candidat à la recette**, pas production. 30 31**Hypothèse alternative examinée :** le README aurait été raccourci par simple réécriture éditoriale, sans intention de retirer la garantie. Elle est plausible — le nouveau texte est manifestement un document produit, pas un document tronqué — mais elle **ne rétablit pas la garantie** : le fait auditable est qu'aucune phrase du dépôt ne rattache plus `backend/` à la production. Non écartée, sans effet sur le constat. 32 33## 0.2 Le sens de la synchronisation s'est inversé — `FAIT` 34 35Au 08/09, `functions/` était **en avance** sur `backend/` : les correctifs y étaient écrits et non déployés. C'était la clé de lecture de toute la famille 4 (`preuves-travail-2026-09-09.md:17`). 36 37Au 14/09, c'est l'inverse sur un point décisif : **`requireTrustedPrincipal` est présent dans 63 fichiers de `backend/` (164 occurrences) et dans ZÉRO fichier de `functions/`**. `backend/` porte donc du code que les sources de travail n'ont pas. 38 39Recomptage indépendant du marqueur : `trustedActor` → 61 fichiers, 84 occurrences en `N`, **0 en `S`**. 40 41**Conséquence de méthode :** la convention du 08/09 (« le défaut est dans `backend/`, la réparation dans `functions/` ») **ne s'applique plus**. Chaque constat doit être vérifié dans `backend/` directement, ce qui a été fait ici. 42 43## 0.3 Ce que la synchronisation a réellement fait — `FAIT` 44 45Sur les 84 entrées communes : **10 sont identiques** à celles du 08/09, **74 ont changé**. 17 fichiers sont neufs. 46 47`backend/ringover-call-events.go` est passé de **4 440 à 4 565 lignes**, et il est désormais **octet pour octet identique** à `functions/ringover-call-events/main.go` (`cmp` → égal). C'est exactement le chiffre que `preuves-travail-2026-09-09.md:70` attribuait à la **source de travail**. La synchronisation a donc déployé la source Go. 48 49Pour les `.mjs`, la relation est différente et il faut la dire précisément : sur 93 `.mjs` de `backend/`, **12 sont identiques à leur source**, **48 en diffèrent**, **33 n'ont pas de source homonyme** (les 23 fonctions commerciales/pilotage dérivées, plus les neuves et les `tmp-*`). 50 51Vérification du contenu de cette divergence sur quatre fichiers porteurs de la famille 4 : la différence `functions/` → `backend/` est **dominée par le bloc de l'adaptateur d'autorisation**, absent des sources. 52 53- `flexylead-app-dashboard-query.mjs` : 65 lignes de différence 54- `flexylead-record-work-result.mjs` : 76 55- `flexylead-claim-missed-call.mjs` : 96 56- `flexylead-start-callback.mjs` : 130 57 58Les premières lignes ajoutées côté `backend/` sont dans les quatre cas le corps de l'adaptateur (`const __authParityEvent = Symbol(...)`, `// Flexylead authorization adapter...`, `const principal = event?.requestContext?.authorizer?.principal;`…). 59 60`SUGGÉRÉ` — et non `FAIT` — **que la totalité** de la divergence soit l'adaptateur : sur `flexylead-start-callback.mjs` les 130 lignes d'écart excèdent nettement la taille du bloc (≈ 45 lignes), donc au moins un autre écart existe sur ce fichier. Il n'a pas été caractérisé. 61 62## 0.4 L'adaptateur d'autorisation : ce qu'il fait, ce qu'il ne couvre pas — `FAIT` 63 64Corps exact, `code/flexylead-crm-2026-09-14/backend/flexylead-app-dashboard-query.mjs:1-17` : 65 66```js 67const __authParityEvent = Symbol("verified-invocation-event"); 68// Flexylead authorization adapter for Functions' reserved invocation context. 69// Business identity is never inferred from event.auth, actor flags or headers. 70function requireTrustedPrincipal(event, context) { 71 const principal = event?.requestContext?.authorizer?.principal; 72 const identity = context?.invocation; 73 const tenant = String(process.env.FLEXYLEAD_AUTH_ORGANIZATION_SLUG || 'default'); 74 if (!principal || identity?.kind !== 'authenticated' 75 || principal.issuer !== `apteva:auth:${tenant}` 76 ... 77``` 78 79Il exige un principal **posé par la passerelle** (`event.requestContext.authorizer.principal`) et le recoupe avec le contexte d'invocation de la plateforme. `trustedActor` (`:19`) va plus loin : il rappelle `auth_users_get_context` et **refuse** que l'acteur déclaré dans le corps diffère de l'utilisateur authentifié (`:35-38`, « Command actor does not match the authenticated user. »). 80 81**C'est un progrès réel, et il est mal placé pour la famille 4.** L'adaptateur ne fonctionne que pour les fonctions **invoquées par la passerelle authentifiée**. Il est structurellement inapplicable à une porte qui reçoit un webhook d'opérateur. Les **37 fichiers sans adaptateur** comprennent donc, et ce n'est pas un hasard : 82 83- les **trois portes** du constat 17 : `flexylead-ringover-smart-routing.mjs`, `ringover-call-events.go`, `flexylead-project-telephony-event.mjs` 84- la porte LeadByte : `leadbyte-event-inspector.mjs` 85- les deux filets audio : `ringover-audio-mirror.mjs`, `ringover-audio-reconcile.mjs` 86- **six des neuf fonctions neuves** : `commercial-report-access.mjs`, `flexylead-discard-missed-call.mjs`, `flexylead-ringover-presence.mjs`, `flexylead-sync.mjs`, `leadbyte-prospect-dispatcher.mjs`, `flexylead-mirror-specific-audios.mjs` 87- les **huit scripts `tmp-*`** et `flexylead-debug-list-drive-folder-reda.mjs` 88 89**Deux régimes d'authentification coexistent maintenant dans le déployé** : l'adaptateur (63 fichiers) et un contrôle maison par jeton porteur revalidé auprès de `FLEXYLEAD_AUTH_BASE_URL/me` (`backend/commercial-report-access.mjs:143-146`, `backend/flexylead-discard-missed-call.mjs`, même fonction `authenticateActor` recopiée). `HYPOTHÈSE` : les fonctions neuves ont été écrites avant, ou hors, du chantier de l'adaptateur. Non testable dans le dépôt, qui ne porte aucun historique par fichier. 90 91## 0.5 Correction au cadrage : les tâches planifiées — `FAIT` 92 93Le cadrage annonce « sept tâches planifiées, dont une neuve : `ringover-presence-refresh.json` ». **Les deux moitiés sont fausses.** 94 95- `ls config/jobs/` → **six** fichiers au 14/09. Six également au 08/09. 96- `diff -r code/flexylead-crm-0908/config/jobs code/flexylead-crm-2026-09-14/config/jobs` → **aucune différence**. `ringover-presence-refresh.json` existait déjà à l'identique le 08/09. 97 98Ce qui est vrai et n'avait pas été relevé : **deux des six tâches ne désignent aucune fonction à invoquer.** `ringover-presence-refresh.json` et `routing-performance-scores.json` n'ont pas de bloc `target` — là où les quatre autres portent `"target": {"kind":"app_tool","tool":"functions_invoke","input":{"name": ...}}`. `routing-performance-scores.json` n'a même pas de `status`. Une tâche sans cible ne peut pas être reliée à une fonction **depuis le dépôt** : le rattachement de `ringover-presence-refresh` à `flexylead-ringover-presence` ne repose que sur le champ `owner_app`. 99 100**Confirmé :** aucune tâche ne vise `flexylead-integrity-audit` ni `ringover-audio-mirror` ni `flexylead-mirror-specific-audios`. 101 102--- 103 104# 1. Verdict par constat 105 106| n° | titre (09/09) | verdict au 14/09 | 107|---|---|---| 108| 14 | Un rendez-vous jamais appelé est enregistré comme injoignable | **INTACT** | 109| 15 | La relance d'un numéro qui n'a pas répondu est morte | **CORRIGÉ** — les trois moitiés déployées | 110| 16 | Un appel pris par le serveur vocal est compté comme un appel pris par un commercial | **PARTIELLEMENT CORRIGÉ — deux compteurs sur quatre** | 111| 17 | Deux portes d'entrée n'authentifient rien | **INTACT** | 112| 18 | Le classement énergie ou télécom est une devinette | **INTACT**, rayon d'impact étendu | 113| 19 | Le consentement du client n'est rangé nulle part | **INTACT** | 114| 20 | Des enregistrements finissent par être introuvables | **INTACT**, légèrement **aggravé** | 115| 21 | Rien ne relie les tables entre elles | **AGGRAVÉ** — 13 → 16 fonctions | 116| 22 | La supervision est tracée d'un côté et pas de l'autre | **PARTIELLEMENT CORRIGÉ** — identité oui, trace non | 117| 23 | Une vente annulée garde son verrou de trois mois | **INTACT** | 118 119## Constat 14 — « Un rendez-vous jamais appelé est enregistré comme injoignable » : **INTACT** · `FAIT` 120 121Les deux écrivains de la même valeur sont toujours là, aux mêmes conditions. 122 123- `expireStaleAIReservations` : `N flexylead-route-work-items.mjs:1140` (définition, était `:1040`), écriture `status:"no_show"` / `commercial_outcome:"unreachable"` à `:1159-1160`, filtre `COALESCE(NULLIF(reservation_type,''),'ai_booking') = 'ai_booking'` à `:1148`, seuil `zonedDayStart` à `:1144`. 124- `buildTransition` : `N flexylead-record-work-result.mjs:187` (définition), écriture `commercial_outcome: reachedMax ? "unreachable_max" : "unreachable"` à `:208`. 125- Consommation : `N flexylead-app-dashboard-query.mjs:138` exclut toujours `'unreachable'` du décompte. 126- Condition d'exécution inchangée : l'expiration ne tourne que dans le passage de maintenance (`N flexylead-route-work-items.mjs:86`, `routingSweepPlan` défini `:172`). 127 128**Nouveauté sans effet sur le constat :** `buildTransition` distingue désormais `unreachable_max` de `unreachable` au 4ᵉ NRP. Cela sépare « quatre échecs constatés » de « un échec constaté », **pas** « jamais appelé » de « appelé sans réponse ». La confusion visée par le constat est entière. 129 130**Élément neuf voisin, qui ne corrige pas ce constat mais mérite d'être connu :** `backend/flexylead-discard-missed-call.mjs` (neuf, déployé) crée un troisième sort pour un appel manqué — `missed_pool_status: "discarded"` — avec un **motif obligatoire** (`:14-16`, refus si < 3 caractères), l'auteur et l'horodatage (`missed_discarded_by`, `missed_discarded_at`, `missed_discard_reason`). C'est le seul endroit de la chaîne où un retrait de file est motivé et attribué. Il ne touche pas les rendez-vous IA. 131 132**Hypothèse alternative examinée :** une colonne distinguant les deux origines existerait ailleurs (par ex. `commercial_outcome_source`). Écartée : le schéma `prospects` ne la porte pas (35 colonnes, `S functions/test-support/table-schema.json`), et les deux écritures ci-dessus posent la même valeur sans champ d'origine. 133 134## Constat 15 — « La relance d'un numéro qui n'a pas répondu est morte » : **CORRIGÉ, et le correctif est COMPLET** · `FAIT` 135 136Les **trois** moitiés identifiées le 09/09 (`preuves-travail-2026-09-09.md:44`) sont en service. 137 138| moitié | au 08/09 | au 14/09 | 139|---|---|---| 140| `buildTransition` libère la fiche | absent de `D2` | **`N flexylead-record-work-result.mjs:226`** — `followup_appel_id: null`, avec le commentaire d'origine « *this slot is reserved for an active attempt, not the previous NRP* » (`:224-225`) | 141| `assertActiveMissedCallClaim` tolère le NRP en attente | absent de `D2` | **`N flexylead-start-callback.mjs:673`** — `const nrpWaiting = followupStatus === "nrp_waiting" && nrpCount > 0 && nrpCount < 4;` puis dérogation `:677` et garde amendée `:685` : `if ((clean(parentCall.followup_appel_id) && !nrpWaiting) || followupStatus === "callback_started")` | 142| `recoverFailedCallbackPointer` | absent de `D2` | **`N flexylead-start-callback.mjs:1396`** (déf.) appelée `:91` · **`N flexylead-claim-missed-call.mjs:904`** (déf.) appelée `:77` | 143 144`MAX_NRP = 4` : `N flexylead-record-work-result.mjs:59`. Le cycle des quatre tentatives peut désormais se dérouler. 145 146**Recomptage du marqueur, qui confirme et précise le cadrage :** `nrpWaiting` → 0 fichier / 0 occurrence en `D2`, **1 fichier / 3 occurrences** en `N`. `recoverFailedCallbackPointer` → 0 / 0 en `D2`, **2 fichiers / 4 occurrences** en `N`. Les chiffres « 1 » et « 2 » du cadrage sont des comptes de **fichiers**, pas d'occurrences. 147 148**Réserve à porter avec le correctif — `FAIT` :** `recoverFailedCallbackPointer` est **recopiée intégralement dans deux fonctions**, avec la même signature `(context, parent, aliases, now = Date.now())`. `backend/` ne contient **aucun répertoire `shared/`**, alors que `functions/shared/` en porte quatre modules dont `callback-recovery.mjs`. Le déploiement aplatit donc les modules partagés en copies. C'est exactement le motif qui a coûté le plus cher au constat 16 (`confirmedBusinessAnswerSql` dupliqué) : **deux exemplaires à corriger au prochain défaut, sans rien qui le signale**. 149 150**Hypothèse alternative examinée :** les deux copies pourraient avoir divergé. Non vérifié ligne à ligne — `limite déclarée`. Les signatures sont identiques ; le corps ne l'a pas été comparé. 151 152## Constat 16 — « Un appel pris par le serveur vocal est compté comme un appel pris par un commercial » : **PARTIELLEMENT CORRIGÉ** · `FAIT` 153 154*Section reprise et durcie après la remontée du poste « données » du 14/09. La trouvaille est exacte ; sa portée est plus étroite qu'elle n'en a l'air, et elle a un effet de bord qui n'avait pas été vu.* 155 156**Deux compteurs sur quatre filtrent désormais l'IVR**, à l'intérieur de la règle qui décide ce qu'est « un appel pris ». 157 158- `N flexylead-app-dashboard-query.mjs:193`, dans `confirmedBusinessAnswerSql` (déf. `:190`) : `lower(COALESCE(CAST(${column('handled_by')} AS TEXT), '')) != 'ivr'` 159- `N flexylead-app-acquisition-query.mjs:62`, dans `confirmedBusinessAnswerSql` (déf. `:59`) : même ligne 160 161**Correction au relevé transmis :** la ligne du tableau de bord est **`:193`**, et non `:191` — `:190` porte la signature de la fonction, `:191` la lambda `column`. La référence `flexylead-app-acquisition-query.mjs:62` est exacte. 162 163**Le point de départ est confirmé, et il est net.** Au 08/09, le corps de la fonction est le même **moins la ligne IVR** : `D2 flexylead-app-dashboard-query.mjs:139-147` et `D2 flexylead-app-acquisition-query.mjs:10-18` commencent directement par le test sur `answered_at`. Recomptage du mot `ivr` sur les quatre compteurs de la famille 1 : 164 165| compteur (famille 1, constat 1) | `ivr` au 08/09 | `ivr` au 14/09 | exclut l'IVR du décroché ? | 166|---|---|---|---| 167| `flexylead-app-dashboard-query` | **0** | 1 | **OUI** — `:193` | 168| `flexylead-app-acquisition-query` | **0** | 1 | **OUI** — `:62` | 169| `flexylead-app-commercial-metrics-query` | **0** | **0** | **NON — inchangé** | 170| `flexylead-app-pilotage-summary-query` | 1 | 4 | **NON** | 171 172**Les deux compteurs non corrigés, preuve à l'appui :** 173 174- `N flexylead-app-commercial-metrics-query.mjs:731` compte le décroché sur la seule présence de l'horodatage : `SUM(CASE WHEN answered_at IS NOT NULL AND CAST(answered_at AS TEXT) <> '' THEN 1 ELSE 0 END) AS answered`. Aucun test sur `handled_by`, et **zéro occurrence de `ivr` dans tout le fichier**. 175- `N flexylead-app-pilotage-summary-query.mjs:2789-2793`, `hasAnswerEvidence` — la fonction qui décide du `answered` de pilotage (`:1805`, `:1395-1400`) : 176 ```js 177 function hasAnswerEvidence(call) { 178 if (clean(call.answered_at)) return true; 179 if (nonNegative(call.talk_duration_seconds) > 0) return true; 180 return ["answered", "completed"].includes(clean(call.call_outcome).toLowerCase()); 181 } 182 ``` 183 **Aucun test sur `handled_by`.** Les quatre occurrences de `ivr` du fichier sont ailleurs : `:1494` est un commentaire français sans rapport, `:4727`, `:4771` et `:4775` sont dans un bloc de **reconstruction d'aiguillage** (`candidates`, `reject("already_answered")`), pas dans un compteur. Et l'unique occurrence du 08/09 (`D2 :1404`) était **le même commentaire** : pilotage n'a donc rien gagné de ce côté. 184 185**C'est le code lui-même qui borne la correction — `FAIT`.** Le commentaire posé à l'écriture de la marque, `N ringover-call-events.go:3032-3033`, nomme ses deux destinataires : 186 187> *« Preserve answered_at as the provider fact, but make the business ownership explicit so Dashboard/Acquisition do not report a pickup. »* 188 189Deux écrans nommés, deux écrans corrigés. Les deux autres compteurs n'ont pas été considérés. 190 191**L'écrivain de la marque est bien celui qu'on croit — `FAIT`.** `isUnattributedIVRAnswer` est défini `N ringover-call-events.go:625-637` et c'est lui qui commande l'écriture `row["handled_by"] = "ivr"` (`:3030-3036`, avec `issue = "ivr_connected_unattributed"` et un `routing_warning`). Il existait déjà au 08/09 (`D2 :611` déf., `:2912-2915` écriture) : le marqueur « 1 occurrence au 08/09 comme au 14/09 » du cadrage est un compte de **fichiers** et masque un enrichissement réel — la version du 14/09 ajoute la garde `handled_by != "ai"` et la **remontée en `"human"`** quand l'opérateur finit par identifier un agent (`:3038-3041`). 192 193**Substitution à porter au document publié.** La phrase du 09/09 — « La marque existe en production […] **Aucun compteur ne la lit** » — devient : 194 195> « La marque existe en production. **Deux des quatre compteurs la lisent depuis le 14 septembre ; les deux autres continuent de compter un appel pris par le serveur vocal comme un appel pris par un commercial.** » 196 197### Effet de bord : rupture de série, et elle est rétroactive · `FAIT` pour le mécanisme, `SUGGÉRÉ` pour l'ampleur 198 199Le point soulevé par le poste « données » est réel, et **plus large que « à compter du 14/09 »**. La marque `handled_by='ivr'` n'est pas neuve : `isUnattributedIVRAnswer` l'écrivait déjà au 08/09. Le filtre ajouté le 14/09 s'applique **au moment de la requête**, sur des lignes déjà marquées. 200 201**Conséquence : ce n'est pas seulement le taux de décroché à venir qui baisse, c'est toute la série historique affichée par ces deux écrans qui baisse d'un coup**, y compris sur des périodes closes. Un mois d'août consulté le 13 et le 15 septembre ne donnera pas le même taux de décroché, sans qu'aucun appel n'ait changé. 202 203`SUGGÉRÉ` et non `FAIT` pour l'ampleur : le nombre de lignes portant `handled_by='ivr'` n'est pas mesurable depuis le dépôt. **La mesure utile, en lecture seule, est un décompte de `appels` par `handled_by` et par mois** — elle donne directement la hauteur de la marche et son étendue dans le passé. 204 205### La rémunération n'est pas touchée — et c'est le vrai problème · `FAIT` 206 207La fonction de rémunération calcule son `answered` avec **la même `hasAnswerEvidence`**, corps identique, à la même ligne : `N flexylead-app-pilotage-remuneration-query.mjs:2789-2793`. Elle ne teste pas `handled_by` (ses seules occurrences de `ivr` sont, à l'identique, `:1494`, `:4727`, `:4771`, `:4775`). 208 209Les six fonctions de pilotage ne sont pas des copies exactes l'une de l'autre (six `md5` distincts) mais partagent le même paquet : 4 866 lignes chacune, mêmes fonctions aux mêmes numéros de ligne. 210 211**Donc :** le changement du 14/09 ne modifie **aucun montant**. Il crée en revanche un écart **nouveau et silencieux** entre ce que le tableau de bord affiche et ce que la rémunération calcule, pour un indicateur qui porte le même nom dans les deux écrans. C'est très exactement le motif du **constat 2**, « Deux compteurs justes portent le même nom ». 212 213Et pour la famille 1 : le constat 1 disait « quatre fonctions comptent les appels reçus, et aucune ne compte comme les autres ». **La divergence entre les quatre s'est creusée**, elle ne s'est pas réduite. Le geste du 14/09 corrige deux écrans et **aggrave l'incohérence d'ensemble**. 214 215**Hypothèse alternative examinée :** les deux compteurs non corrigés ne mesureraient pas la même chose et n'auraient pas à exclure l'IVR. Écartée pour `flexylead-app-commercial-metrics-query` — il produit bien un `answered` par commercial et par jour (`:731`), destiné à l'écran du commercial, et un appel IVR n'y est imputable à personne. Non écartée pour le bloc `:4727` de pilotage, qui reconstruit un aiguillage et où l'exclusion de `["ai","ivr"]` est légitime. 216 217### Le reste du dispositif IVR 218 219Le dispositif du 09/09 est déployé dans au moins **sept** des huit fichiers recensés : dashboard `:193`, acquisition `:62`, `route-work-items` `:201,210,1283`, `claim-missed-call` `:599`, `project-telephony-event` (`projectAiCallbackObligation` `:327,781`), `reconcile-telephony-handoffs` (`:250,1183`), `ringover-call-events.go`. 220 221Deux bornes : 222 2231. `FAIT` — **`flexylead-ringover-smart-routing.mjs` ne porte aucune occurrence de `ivr`** alors qu'il figurait dans les huit fichiers du correctif source (`preuves-travail-2026-09-09.md:56`). Le 8ᵉ fichier n'a pas reçu sa part. 2242. `FAIT` — `projectAiCallbackObligation` est, elle aussi, **recopiée dans deux fichiers** (`project-telephony-event.mjs:781` et `reconcile-telephony-handoffs.mjs:1183`). Même dette que `recoverFailedCallbackPointer`. 225 226**Ce que les +132 lignes du Go ont fait — `FAIT`, et c'est la réponse au point 2 du cadrage.** Le diff `D2` → `N` sur `ringover-call-events.go` compte 139 lignes modifiées. Elles ajoutent `reconcileConfirmedAIHandling` (rapprochement d'un appel Ringover et d'un appel Twilio voisin à ±8 s pour confirmer une prise en charge par l'IA), `aiHandlingRepairable`, la structure `tableRows`, et la garde `isUnattributedIVRAnswer` dans le chemin de décision. Commentaire porté par le code : « *Only fill handling metadata when both providers confirm a unique pickup. No ownership, caller, outcome, interaction or routing fields are rewritten.* » 227 228**Les +132 lignes sont donc du constat 16, pas du constat 17.** Elles ne contiennent aucune authentification. 229 230## Constat 17 — « Deux portes d'entrée n'authentifient rien » : **INTACT** · `FAIT` 231 232Recomptage sur le déployé du 14/09, `backend/ringover-call-events.go` : 233 234| marqueur | compte | 235|---|---| 236| `signature` | **0** | 237| `hmac` | **0** | 238| `secret` | **0** | 239| `os.Getenv` | **0** | 240| `.Headers` (la carte d'en-têtes entrants) | **0** | 241| `.Header` | 3 | 242 243Les trois `.Header` sont **sortants et sans rapport avec l'authentification entrante** : `N ringover-call-events.go:4066` et `:4076` posent un `user-agent` sur une requête de téléchargement d'audio, `:4092` lit le `content-type` de la **réponse**. La porte reçoit toujours son événement sans rien vérifier, et le champ prévu pour cela n'est toujours relu nulle part — désormais sur **4 565 lignes en service** (et non 4 440). 244 245**Substitution à porter au document :** la phrase du 09/09 « ni dans les 4 440 lignes en service ni dans les 4 565 lignes de la source » n'a plus d'objet : les deux nombres ont fusionné. Le texte publiable est « **et n'est relu nulle part dans les 4 565 lignes de la fonction** ». 246 247Porte 1 inchangée : `verifyRingoverSignature` toujours appelée avant tout traitement, et `flexylead-ringover-smart-routing` toujours déclarée **sans `disable_function_url`** au manifeste — `N scripts/application-api-manifest.mjs:74` : 248 249```js 250{ name: 'flexylead-ringover-smart-routing', file: '../../functions/flexylead-ringover-smart-routing.mjs', timeout_ms: 15_000, max_memory_mb: 256 }, 251``` 252 253à comparer aux 20 lignes voisines qui portent toutes `disable_function_url: true`. Le manifeste est passé de 68 à **71 déclarations**. 254 255Porte 3 inchangée : `grep -cniE "authenticate|signature|hmac|api_key|secret|token"` sur `N flexylead-project-telephony-event.mjs` → **0**, et la fonction reste **absente du manifeste** (0 occurrence). Son exposition publique n'est toujours pas établie depuis le dépôt. 256 257**L'asymétrie ne survit pas seulement à la synchronisation : elle est désormais inscrite dans le chantier d'autorisation.** Les trois portes du constat 17 sont dans les 37 fichiers **sans** `requireTrustedPrincipal`. 63 fonctions ont reçu un adaptateur d'autorisation ; aucune des portes qui reçoivent l'extérieur ne l'a reçu. 258 259**Hypothèse alternative, sérieuse et non écartée :** l'adaptateur est **techniquement inapplicable** à une porte de webhook. Il exige `event.requestContext.authorizer.principal`, c'est-à-dire un principal posé par la passerelle authentifiée ; un opérateur téléphonique n'en pose pas. L'absence d'adaptateur sur les trois portes n'est donc **pas** une négligence du même ordre que son absence sur une fonction métier. Le constat 17 tient — ces portes n'authentifient rien — mais **le rapprocher du chantier de l'adaptateur serait une erreur de lecture** : ce chantier ne pouvait pas les couvrir. Le remède reste celui du 09/09 : une signature, côté Ringover, sur le modèle de `verifyRingoverSignature`. 260 261**Point resté ouvert depuis le 09/09 :** la contradiction interne « le titre dit deux, le corps en traite trois » (`relecture-coherence-2026-09-09.md:32`) n'est pas résolue et ne l'est pas par le code. C'est un arbitrage de rédaction. 262 263## Constat 18 — « Le classement énergie ou télécom est une devinette » : **INTACT** · `FAIT` 264 265Le cœur est inchangé, au mot près. `N leadbyte-event-inspector.mjs:369-370` : 266 267```js 268if (/energie|energy|electric|électric|gaz|fournisseur|compteur|\bpdl\b|\bpce\b/.test(haystack)) return "energie"; 269if (/telco|telecom|télécom|operateur|opérateur|internet|fibre|forfait|\bbox\b/.test(haystack)) return "telco"; 270``` 271 272L'énergie est toujours testée en premier, le foin comprend toujours les clés et valeurs arbitraires du formulaire (`:364-368`), et le repli est toujours `LEADBYTE-UNCLASSIFIED` (`:378`). Le second classeur, mieux écrit et à mots bornés, est toujours dans le même fichier et toujours second (`inferDispatchCategory`, `:1026`, tests `:1041-1042`). 273 274Le correctif source **n'est pas déployé** : `preciseTelecomOfferId` → **0 occurrence** dans `N`, **11 occurrences** dans `S` (`functions/flexylead-sync-local-google.mjs:2922` déf., `:3136` appel, `:5699` export). Le fichier `backend/flexylead-sync-local-google.mjs` est **identique** à celui du 08/09 : il n'a pas été touché par la synchronisation. Le catalogue déployé porte toujours `free_box` et `free_mobile` **sans `type_offre`** (`N flexylead-sync-local-google.mjs:485-486`), là où les offres énergie voisines en ont un (`:479`, `:482`). 275 276**Le marqueur du cadrage est confirmé : 0 au 08/09, 0 au 14/09.** 277 278**Rayon d'impact étendu — `FAIT` :** une fonction neuve déployée, `N leadbyte-prospect-dispatcher.mjs` (440 lignes, identique à sa source), **consomme** `dispatch_category` (`:206`, `:308`, `:420`) et **crée la colonne elle-même** (`:109`, dans un `tables_create`). Elle ne classe pas et n'offre aucun reclassement : elle distribue sur la foi de la devinette. Un consommateur de plus, et un écrivain de schéma de plus (cf. constat 21). 279 280**Hypothèse alternative examinée :** le dispatcher corrigerait le classement en amont. Écartée par lecture : `grep -n "inferFlat|inferDispatch|categor"` sur le fichier ne ramène que des lectures et la déclaration de colonne, aucune fonction de classement. 281 282## Constat 19 — « Le consentement du client n'est rangé nulle part » : **INTACT** · `FAIT` 283 284Le marqueur `consent` vaut 1 fichier au 08/09 comme au 14/09 — et ce fichier n'est pas un stockage. 285 286- Les **12 occurrences** de `consent` dans `N` sont **toutes** dans `backend/process-appel-db-only.mjs` (`:111`, `:1225`, `:1885`, `:1887`, `:1889`, `:1890`, `:1919`, `:1920`, `:2162`, `:2186`, `:2752`). Ce sont des **consignes en français adressées au modèle de notation**, pas des colonnes. 287- Les occurrences de `optin` / `opt-in` hors de ce fichier sont des **commentaires de code sans rapport** : « *Opt-in : un appelant ancien reçoit l'objet complet* », répété à `:932` dans une vingtaine de fonctions commerciales — il s'agit de la taille des réponses HTTP, pas du consentement client. 288- La table `prospects` compte **35 colonnes** (`S functions/test-support/table-schema.json`, table `prospects`) : `telephone, email, nom, prenom, adresse, source, statut, assigned_commercial_id, last_appel_id, next_callback_at, notes, created_at_iso, updated_at_iso, campaign_id, leadbyte_lead_id, leadbyte_sid, leadbyte_ssid, leadbyte_supplier_id, leadbyte_supplier_name, leadbyte_campaign_id, leadbyte_campaign_ref, leadbyte_last_event_id, dispatch_due_at, assigned_at, assignment_source, assignment_error, qualification_cycle, nrp_count, lead_hidden_until, last_qualification_event, last_qualification_at, qualification_status_reason, centre_lock_id, centre_lock_until, centre_lock_source`. **Aucune ne porte un accord.** (34 au 08/09 ; la colonne ajoutée n'est pas un consentement.) 289 290La chaîne de destruction est intacte : `deliveryCustomFields` (`N leadbyte-event-inspector.mjs:343`) rejette les clés sensibles via `isSensitiveKey` (`:350`, déf. `:1108`), `leadByteNotes` (`:1011`) sérialise, `compactText` (`:1085`) tronque à **1 000 caractères**. La substitution la plus dure du 09/09 — l'accord courriel et l'accord téléphonique sont supprimés avant d'atteindre les notes, seul l'accord SMS peut survivre — **reste exacte**. 291 292## Constat 20 — « Des enregistrements finissent par être introuvables » : **INTACT**, légèrement **aggravé** · `FAIT` 293 294- `backend/ringover-audio-mirror.mjs` est **identique** à celui du 08/09. Toujours `order_by: 'id desc'` (`:11`, du plus récent au plus ancien), toujours l'exclusion définitive (`:65`). 295- **Toujours déclenché par rien :** `grep -rn "ringover-audio-mirror"` sur tout le dépôt du 14/09 → **une seule occurrence**, son propre test (`functions/ringover-audio-mirror.test.mjs:3`). Absent du manifeste (0), des six tâches, des cinq flux (`workflows/` en contient cinq, qui invoquent `flexylead-route-work-items` ×2, `flexylead-mark-appel-ready-for-vente` ×2 et `process-appel-db-only`). 296- **Aucune fonction ne remet `storage_status` à zéro** : recherche `storage_status` croisée avec `null|''|delete` sur `N backend/` → **0 résultat**. Les écritures vont vers `mirrored` (`ringover-audio-mirror.mjs:34`, `ringover-audio-reconcile.mjs:187`, `ringover-call-events.go:4261`), `unavailable` (`ringover-audio-reconcile.mjs:462`), `mirror_failed` (`ringover-call-events.go:4296`), `stored` (`flexylead-project-telephony-recording.mjs:122`). L'exclusion reste définitive. 297 298**La fonction neuve ne change pas le sort du constat — `FAIT`.** `N flexylead-mirror-specific-audios.mjs` télécharge un fichier depuis Google Drive et le téléverse dans le stockage, puis met l'appel en `pending_transcript` si **tous** ses fichiers sont stockés. Mais : 299 300- elle **n'a aucun mécanisme de sélection** : elle exige `event.appel_row_ids` et lève `appel_row_ids is required` s'il manque. C'est un outil à alimenter à la main, pas un filet ; 301- elle **ne lit pas `storage_status`** : sa condition est `if (file.storage_file_id || !file.drive_file_id) continue;`. Un enregistrement marqué `unavailable` par `markRecordingUnavailable` **n'est pas récupéré** par elle si son `storage_file_id` est déjà posé, et elle ne remet jamais le statut à zéro ; 302- elle est **référencée nulle part** : `grep -rn "mirror-specific"` sur tout le dépôt (hors le fichier lui-même) → **0 résultat**. Ni manifeste, ni tâche, ni flux, ni test. 303 304**Aggravation :** le déployé porte désormais **trois** chemins de recopie audio (`ringover-audio-mirror`, `ringover-audio-reconcile`, `flexylead-mirror-specific-audios`), dont **deux ne sont déclenchés par rien** et **un seul a une tâche** (`config/jobs/ringover-audio-reconcile.json`, 300 s). Le nom `mirror-specific-audios` et l'absence de test suggèrent une intervention ponctuelle laissée en production. 305 306`HYPOTHÈSE` : cette fonction et les huit `tmp-*` seraient les traces d'une reprise manuelle de données (les `tmp-*` lisent des feuilles Google nommées « Reda », « MGK », « AZS » et comparent des contrats d'avril). Cohérent avec les noms et le contenu, **non établi** : rien dans le dépôt ne les date ni ne les motive. 307 308## Constat 21 — « Rien ne relie les tables entre elles » : **AGGRAVÉ** · `FAIT` 309 310Le chiffre publié doit changer. **Treize fonctions au 08/09, seize au 14/09.** 311 312`grep -rlE "tables_alter|tables_create"` sur `backend/` : 313 314| 08/09 (13) | 14/09 (16) | 315|---|---| 316| commercial-supervision-session, flexylead-admin-data-mutation, flexylead-claim-missed-call, flexylead-manage-facturation, flexylead-manage-stats-periods, flexylead-record-work-result, flexylead-reserve-sale-contracts, flexylead-sync-local-google, flexylead-update-sale, lead-qualification-update, leadbyte-event-inspector, process-appel-db-only, ringover-audio-reconcile | les 13, **plus `flexylead-discard-missed-call`, `flexylead-sync`, `leadbyte-prospect-dispatcher`** | 317 318Les trois ajouts sont des **fonctions neuves**, et toutes trois créent une table ou une colonne en pleine requête : `N flexylead-discard-missed-call.mjs` crée `commercial_missed_call_claims` (`ensureClaimTable`, avec l'avalage classique de l'erreur `already exists`), `N leadbyte-prospect-dispatcher.mjs:109` déclare `dispatch_category`. 319 320Le compte des **sources** est passé de 15 à 15 : c'est bien le **déployé** qui a rattrapé puis dépassé les sources. 321 322**Conséquence pour le document publié :** la phrase « treize fonctions modifient la structure de la base en pleine requête », que `preuves-travail-2026-09-09.md:227` demandait expressément de **conserver telle quelle** parce qu'elle était exactement celle du déployé, **est désormais fausse**. Elle doit devenir « seize ». 323 324La cause amont (la couche `tables` d'Apteva n'émet ni `FOREIGN KEY` ni `UNIQUE` ni `CHECK`) n'a **pas été revérifiée ici** : elle relève d'un autre dépôt (`code/apteva-platform/`), hors du périmètre de cette reprise. `limite déclarée`. 325 326## Constat 22 — « La supervision est tracée d'un côté et pas de l'autre » : **PARTIELLEMENT CORRIGÉ** · `FAIT` 327 328Le marqueur `commercial-supervision-session` est inchangé (2 fichiers avant et après), et c'est trompeur : **le fichier a changé**, de 570 à 619 lignes. 329 330**Ce qui s'est amélioré.** Les **deux** portes sont désormais sous l'adaptateur d'autorisation : 331 332- porte 1, `N commercial-supervision-session.mjs:4` et `:24` — `requireTrustedPrincipal` ; 333- porte 2, `N flexylead-app-commercial-bootstrap-query.mjs` — présente dans les 63 fichiers de l'adaptateur. 334 335L'identité de celui qui consulte ne peut donc plus être déduite d'un drapeau posé dans le corps de la requête (« *Business identity is never inferred from event.auth, actor flags or headers* »). C'est un gain réel de sécurité. 336 337**Ce qui n'a pas bougé — le cœur du constat.** La porte 1 garde son dispositif complet : `AUDIT_TABLE = "commercial_supervision_sessions"` (`:52`), `MAX_TTL_SECONDS = 15 * 60` (`:57`), écriture d'audit (`rows_insert`, `:115`), validation à chaque usage (`verifiedSession`, `:320`). La porte 2, elle : 338 339- `N flexylead-app-commercial-bootstrap-query.mjs:1764` — `resolveCommercial` 340- `:1771-1772` — `canSeeAll = actor.permissions.has('commercial:all_centres')` ; `canSupervise = actor.permissions.has('commercial:team:supervise') && même centre` 341- `:1773` — `if (!canSeeAll && !canSupervise) throw httpError(403, ...)` 342- **`grep -c "commercial_supervision_sessions|rows_insert"` sur ce fichier → 0** 343 344**Sans jeton, sans durée, sans écrire une ligne.** Une réponse à « qui a consulté le portefeuille de X ? » reste incomplète, et rien ne l'indique. 345 346**La fonction neuve n'aggrave pas le constat — `FAIT`, et c'est contraire à ce qu'on pouvait craindre.** `N commercial-report-access.mjs` (194 lignes) : 347 348- **0 écriture** (`grep -c "rows_insert|rows_update|rows_delete"` → 0) — elle ne trace rien, mais elle ne donne accès à rien d'autre que soi ; 349- **elle refuse l'identité d'autrui** : `:114` — `if (requestedCommercialId && !aliases.has(requestedCommercialId)) throw new Error("Identité commerciale refusée.");` ; 350- **elle refuse la supervision** : `:24-26` — « Utilisez la session de supervision pour consulter ces rapports. » si `supervision_token` ou `supervision.read_only` est présent. 351 352Ce n'est donc **pas** un troisième chemin de lecture du portefeuille d'autrui. En revanche elle **n'utilise pas l'adaptateur** et s'authentifie par son propre chemin (`authenticateActor`, jeton porteur revalidé auprès de `/me`, `:143-146`), et elle est **absente du manifeste** — son exposition n'est pas déclarée, comme la porte 3 du constat 17. 353 354**La gravité « Mineure » reste contestée**, pour la raison exacte du 09/09 : le second chemin de lecture du portefeuille d'un autre commercial demeure non tracé. C'est un jugement, pas un fait ; il appartient à l'agent principal. 355 356## Constat 23 — « Une vente annulée garde son verrou de trois mois » : **INTACT** · `FAIT` 357 358Le marqueur est stable et je le confirme au recomptage : `centre_lock_source` → **81 occurrences dans 32 fichiers**, au 08/09 comme au 14/09. 359 360Les quatre phrases du constat tiennent, aux nouvelles lignes : 361 362- **Quatre-vingt-dix jours depuis la date de vente** : `N flexylead-create-sales.mjs:651` (`saleLockUntil`), `:655` — `new Date(safeSoldAt + 90 * 24 * 60 * 60 * 1000).toISOString()`. Écriture `:492-494` (`centre_lock_id`, `centre_lock_until`, `centre_lock_source: "sale"`). 363- **L'annulation ne libère rien** : `grep -c "centre_lock"` sur `N flexylead-update-sale.mjs` → **0**. Inchangé. 364- **Le libérateur ne connaît que deux des cinq origines** : `N lead-qualification-update.mjs:961-962` — 365 ```js 366 const releasesAnsweredCallAffinity = ["answered_call", "commercial_comment"] 367 .includes(clean(prospect?.centre_lock_source).toLowerCase()); 368 ``` 369 et la libération `:978-980` (`centre_lock_id: null`, `centre_lock_source: "released_ko"`) n'a lieu que sous `if (eventName === "ko")` (`:960`) **et** sous cette garde. **`sale` et `work_assignment` ne sont jamais libérés.** 370- **Le verrou expire seul** : `N flexylead-ringover-smart-routing.mjs:2376` — `(LOWER(COALESCE(p.centre_lock_source,'')) = 'sale' AND p.centre_lock_until > ?)`. 371 372**Les cinq origines, aux lignes du 14/09** : `sale` (`flexylead-create-sales.mjs:494`, `flexylead-start-callback.mjs:1277`, `flexylead-reconcile-telephony-handoffs.mjs:600,625`, **`flexylead-sync-local-google.mjs:4463,4485,4492`**), `answered_call` (`ringover-call-events.go:1411,1541,1709`), `commercial_comment` (`lead-qualification-update.mjs:857`), `work_assignment` (`reconcile-telephony-handoffs.mjs:630`), `released_ko` (`lead-qualification-update.mjs:980`). Propagation sans décision : `flexylead-claim-missed-call.mjs:248,268`, `flexylead-route-work-items.mjs:913`. 373 374**Précision non relevée le 09/09 — `FAIT` :** `flexylead-sync-local-google.mjs` pose un verrou d'origine `sale` à **trois endroits**. C'est la synchronisation des feuilles Google, et c'est un **quatrième** écrivain de verrou de vente, hors du chemin de vente proprement dit. Un verrou de 90 jours peut donc naître d'une ligne de tableur. 375 376--- 377 378# 2. Les cinq faits neufs du cadrage — réponse point par point 379 380| # | affirmation du cadrage | verdict | 381|---|---|---| 382| 1 | adaptateur sur 63/100 fichiers de `backend/`, 0 dans `functions/` | **CONFIRMÉ** (63 fichiers, 164 occurrences ; `trustedActor` 61/84 ; 0 en `S`). Il ne couvre **aucune** des trois portes du constat 17, ni la porte LeadByte. Sur le constat 22, il couvre **les deux** portes — l'identité est verrouillée, la **trace** ne l'est pas. | 383| 2 | le webhook non signé n'a rien gagné ; les +132 lignes ? | **CONFIRMÉ**, et les +132 lignes sont **du constat 16** : `reconcileConfirmedAIHandling`, `aiHandlingRepairable`, gardes IVR/IA. Zéro authentification. Les 3 `.Header` sont sortants (`:4066,4076,4092`). **Correction au cadrage :** sur `flexylead-ringover-smart-routing.mjs`, `signature` = **5** occurrences (pas 13), `hmac` = **0** (pas 3), `secret` = **6** (pas 5) — et **identiques au 08/09**. L'asymétrie est réelle ; les chiffres annoncés ne le sont pas. | 384| 3 | miroir audio toujours sans déclencheur ; que fait la fonction neuve ? | **CONFIRMÉ**. `flexylead-mirror-specific-audios` recopie un audio Drive → stockage **sur liste d'identifiants fournie à la main**, ne lit pas `storage_status`, ne le remet jamais à zéro, et n'est **référencée nulle part**. Elle **ne change pas** le sort du constat 20 ; elle ajoute un troisième chemin sans déclencheur. | 385| 4 | sept tâches, dont `ringover-presence-refresh.json` neuve | **INFIRMÉ sur les deux points.** **Six** tâches, et `diff -r` entre les deux livraisons → **aucune différence**. Ce fichier existait à l'identique le 08/09. Fait neuf réel : **deux des six tâches n'ont pas de bloc `target`** et ne désignent donc aucune fonction. Confirmé : aucune tâche ne vise le vérificateur d'intégrité ni le miroir audio. | 386| 5 | fonctions neuves qui écrivent | **CONFIRMÉ**. Effets par constat : `leadbyte-prospect-dispatcher` → consomme la devinette du **18** sans la corriger, et devient un 15ᵉ écrivain de schéma (**21**) ; `flexylead-discard-missed-call` → n'affecte pas **14/15**, mais ajoute un retrait **motivé et attribué** et un 14ᵉ écrivain de schéma ; `flexylead-ringover-presence` → écrit **`presence_status`** uniquement, **0 occurrence de `availability_status`** : **le retrait R4 tient et se renforce** ; `commercial-report-access` → **0 écriture**, mais **refuse** l'identité d'autrui : n'aggrave pas le **22** ; `flexylead-sync` → 16ᵉ écrivain de schéma. Les **huit `tmp-*`** sont des sondes de mise au point sur feuilles Google, sans authentification, présentes dans l'instantané de production. | 387 388**Sur le retrait R4, précision utile — `FAIT` :** au 14/09, les écrivains de `presence_status` sont **deux** (`N ringover-call-events.go:3305` et `N flexylead-ringover-presence.mjs:270`) et les écrivains de `availability_status` **deux autres** (`flexylead-commercial-availability.mjs`, `flexylead-ringover-smart-routing.mjs`). L'aiguilleur lit `availability_status` (4 occurrences) et **jamais** `presence_status` (0 occurrence). Les deux colonnes restent **sans aucun code qui les relie**. Le retrait R4 était juste ; il l'est encore avec un écrivain de plus. 389 390--- 391 392# 3. Ce que je n'ai pas pu établir 393 3941. **Que `backend/` soit le déployé.** C'est le point le plus lourd. Plus aucune phrase du dépôt ne l'affirme (§ 0.1), et `CANONICAL_SOURCE.json` dit « staging-candidate ». Tous les verdicts ci-dessus sont donc **conditionnels à cette déclaration de Marco**. Si `backend/` n'est pas la production, les constats 15 et 16 ne sont pas corrigés en production, seulement dans un instantané. 3952. **La totalité de l'écart `backend/` ↔ `functions/`** sur les 48 fichiers `.mjs` divergents. Vérifié sur quatre ; l'adaptateur domine ; sur `flexylead-start-callback.mjs` un écart résiduel non caractérisé subsiste. 3963. **Si les deux copies de `recoverFailedCallbackPointer`** (et les deux de `projectAiCallbackObligation`) sont identiques ligne à ligne. Signatures identiques, corps non comparés. 3974. **L'ampleur de la rupture de série du constat 16.** Le mécanisme est établi ; le nombre de lignes `handled_by='ivr'` et leur répartition dans le temps ne sont pas mesurables depuis le dépôt. Mesure à faire en lecture seule : décompte de `appels` par `handled_by` et par mois. 3985. **La cause amont du constat 21** (absence de clés étrangères dans la couche `tables`) : non revérifiée, elle vit dans `code/apteva-platform/`. 3996. **Le rattachement de `ringover-presence-refresh.json` à `flexylead-ringover-presence`** : la tâche n'a pas de `target`, le lien ne repose que sur `owner_app`. 4007. **La date et le motif des huit `tmp-*`** et de `flexylead-mirror-specific-audios` : le dépôt ne porte aucun historique par fichier. 401 402--- 403 404# 4. Contre-lecture — ce qui, dans le corpus, contredirait mes conclusions 405 406- **Le verdict « CORRIGÉ » du constat 15 et le « partiellement corrigé » du 16 sont les plus fragiles du lot.** Ils reposent entièrement sur l'hypothèse que `backend/` = production, et c'est précisément la garantie qui a disparu du README ce matin. Si Marco a synchronisé `backend/` **depuis ses sources locales** plutôt que **depuis la production**, alors le geste du 14/09 aurait rendu `backend/` **moins** représentatif de la production qu'il ne l'était le 08/09 — et deux constats seraient déclarés corrigés à tort. Le fait que `ringover-call-events.go` soit devenu **octet pour octet identique** à `functions/ringover-call-events/main.go` est compatible avec les deux lectures : soit la source a été déployée, soit la source a été recopiée par-dessus l'instantané. **Rien dans le dépôt ne départage.** C'est la question à poser à Marco en premier. 407- **Le raisonnement « 63 fichiers protégés, les 3 portes non » se retourne.** L'adaptateur exige un principal de passerelle ; une porte de webhook n'en a pas. Présenter les deux faits côte à côte suggérerait une négligence là où il y a une contrainte technique. Le constat 17 doit rester autonome. 408- **Trois chiffres du cadrage ne se reproduisent pas** (`signature` ×13, `hmac` ×3 sur `flexylead-ringover-smart-routing` ; sept tâches), et **un quatrième relevé transmis était décalé de deux lignes** (`:191` au lieu de `:193`). Le document du 09/09 était déjà tombé sur ce motif — trois chiffres publiés qui étaient ceux de la source et non du déployé (`preuves-travail-2026-09-09.md:227`). **Aucun chiffre ne doit être repris sans recomptage.** Le fond de la remontée du poste « données » était juste ; sa référence de ligne ne l'était pas. 409- **Le constat 21 est le seul dont le chiffre publié devient faux par le seul passage du temps** (13 → 16). Il illustre qu'un constat chiffré sur un instantané doit porter la date de son instantané, sans quoi il se périme silencieusement. 410- **Un correctif partiel peut aggraver le défaut voisin.** Le cas du constat 16 est exemplaire : corriger deux compteurs sur quatre améliore deux écrans et **creuse** l'incohérence dénoncée par les constats 1 et 2. Un remède doit être évalué sur l'ensemble des consommateurs, pas sur celui qu'il vise. 411 412--- 413 414# 5. Ce qui relève d'autres postes — à ne pas trancher ici 415 416- **`audit-donnees` et `audit-argent`, conjointement et en priorité** : la rupture de série du constat 16. Deux compteurs sur quatre excluent désormais l'IVR, **rétroactivement**, tandis que la fonction de rémunération continue de compter avec `hasAnswerEvidence`, qui ignore `handled_by`. Aucun montant ne bouge ; l'écart affiché/payé, lui, s'ouvre. C'est un point de jonction entre les familles 1, 2 et 4 et il doit être arbitré par l'agent principal, pas par moi. 417- **`audit-integrations`** : la signature du webhook Ringover (console Ringover, hors dépôt — point 2 de « ce que je n'ai pas pu établir » du 09/09, toujours ouvert) ; la nouvelle fonction `flexylead-ringover-presence` (867 lignes, cadence annoncée de 30 s) et son rapport au système de présence de la plateforme ; `leadbyte-prospect-dispatcher` et le contrat de réponse à LeadByte ; les huit `tmp-*` qui appellent l'intégration `google-sheets` sans authentification propre ; `flexylead-sync` (2 439 lignes, dépendance `@composio/core`). 418- **`audit-donnees`** : les 16 fonctions qui altèrent le schéma en pleine requête ; la 35ᵉ colonne de `prospects` ; les trois chemins de recopie audio et la cohérence de `storage_status`. 419- **`audit-agents-ia`** : `flexylead-admin-mcp.mjs` (1 497 lignes, **neuf**, déployé, **sans adaptateur d'autorisation**, absent du manifeste) — une porte MCP d'administration apparue dans l'instantané de production. Hors de mon périmètre, mais elle n'a été relevée par personne et elle mérite un examen à part. 420- **`audit-qualite-ia`** : `process-appel-db-only.mjs` a bougé (3 962 → 3 972 lignes) et `process-appel.mjs` est apparu — la famille 3 les tenait pour identiques entre livraisons (`preuves-notation-2026-09-09.md:17-21`). **Cette garantie est à revérifier.** 421- **`audit-contradicteur`** : le § 4 ci-dessus, et en particulier la question « synchronisé depuis la production ou depuis les sources locales ? ». 422