Preuve · rapport

preuves-chiffres-2026-09-09.md

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

Lignes citées par l'audit : 29 34 39 43 48

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

1# Preuves — Famille 1 « Les chiffres » + cellules de carte associées
2
3Vérification phrase par phrase du document `audit-v3.html`, section « Les constats », famille 1, et des deux cellules de la carte portant sur les compteurs.
4Dépôts en lecture seule : `code/flexylead-crm-0908/` (`730a5e79`, référence), `code/flexylead-crm/` (`888cf02`), `code/apteva-platform/`.
5Établi le 09/09/2026 — `audit-donnees`.
6
7## Avertissement de méthode, préalable à tout ce qui suit
8
9**`backend/` = ce qui tourne. `functions/` = la source de travail, postérieure et non déployée.**
10Source : `code/flexylead-crm-0908/README.md` — « `backend/` contains deployed-source snapshots; these are not the editing source of truth. The working sources include local changes that are not necessarily deployed ».
11Les instantanés déployés des deux dépôts sont **identiques** sur tous les fichiers cités ici (`md5` comparé sur `flexylead-app-pilotage-summary-query.mjs`, `flexylead-app-dashboard-query.mjs`, `flexylead-app-commercial-calls-query.mjs`, `flexylead-integrity-audit.mjs`, `ringover-call-events.go`). Le déployé est donc stable entre le 888cf02 et le 730a5e79.
12
13**Le code déployé est engendré en plusieurs exemplaires.** Trois familles, à une constante près :
14
15| Famille | Fonctions déployées | Fichier source unique | Preuve |
16|---|---|---|---|
17| commercial | 23 (`flexylead-app-commercial-*-query`) | `functions/flexylead-app-commercial-resource-query.mjs` | `diff backend/flexylead-app-commercial-bootstrap-query.mjs backend/flexylead-app-commercial-calls-query.mjs` → **une seule ligne** : `const RESOURCE` ; `scripts/application-api-manifest.mjs:12,23` |
18| pilotage | 6 (`flexylead-app-pilotage-*-query`) | `functions/flexylead-app-pilotage-query.mjs` | `diff backend/flexylead-app-pilotage-remuneration-query.mjs backend/flexylead-app-pilotage-summary-query.mjs` → **une seule ligne** : `const DEPLOYED_RESOURCE` ; `scripts/application-api-manifest.mjs:37-42` |
19| operations | 3 (`flexylead-app-operations-*-query`) | `functions/flexylead-app-operations-config-query.mjs` | même en-tête, 232 lignes chacune |
20
21**Conséquence pour le rendu :** tout comptage « par fichier » sur `backend/` est trompeur. Et « la famille des fonctions de pilotage » est la bonne formulation ; `flexylead-app-pilotage-remuneration-query` en est un membre nommable sans erreur.
22
23---
24
25## Tableau de vérification
26
27| # | Phrase exacte du document | Verdict | Fonction(s) à nommer dans le texte | Preuve `fichier:ligne` | Texte de remplacement si nécessaire |
28|---|---|---|---|---|---|
29| **1.0** | « Personne ne peut dire combien d'appels ont été reçus » (titre) | **À BORNER** | — | Ce qui est prouvé (1.1 à 1.3) : quatre fonctions répondent différemment à la même question. Ce qui n'est pas prouvé : que personne ne le puisse — aucune exécution en production n'a été faite. | « Quatre fonctions comptent les appels reçus, et aucune ne compte comme les autres » |
30| **1.1** | « Le mot "manqué" a trois définitions selon l'écran. » | **VRAI** (au moins trois ; j'en documente quatre) | `queryCallAggregates` (dans `flexylead-app-dashboard-query`) · `metricsResource` (famille des fonctions commerciales, p. ex. `flexylead-app-commercial-metrics-query`) · `queryAcquisitionFacts` (dans `flexylead-app-acquisition-query`) | **A** `backend/flexylead-app-dashboard-query.mjs:224,319` — `missed` = `answered = 0 AND missed_marker = 1`, où `missed_marker` teste le **premier champ non vide** parmi `issue`, `motif`, `status` contre 5 valeurs.<br>**B** `backend/flexylead-app-commercial-metrics-query.mjs:665` — `SUM(CASE WHEN lower(CAST(status AS TEXT)) IN ('manque','missed') THEN 1 ELSE 0 END)` : `status` seul, 2 valeurs, **sans** condition de non-réponse, **sans** filtre `parent_appel_id`.<br>**C** `backend/flexylead-app-acquisition-query.mjs:174` — `missed_new` = `is_new = 1 AND period_answered = 0` : aucun statut lu, restreint aux campagnes publicitaires appariées.<br>**D** (quatrième) `backend/flexylead-app-pilotage-summary-query.mjs:3164,3304` — « répondu » défini par la **négation** de `('appel_manque','missed','no_answer')` sur `issue`/`motif`. | Le chiffre « trois » peut rester si l'on ne parle que des trois écrans nommés. Sinon : « quatre définitions » |
31| **1.2** | « Un seul appel qui sonne chez trois personnes laisse trois lignes, que le code refuse volontairement de regrouper. » | **VRAI** | `findExistingAppel` et `upsertAppel` (dans `ringover-call-events`) — pour la création des lignes ; `suppressMissedSiblingChannelsAfterAnswer` — pour ce qu'il en advient ; `queryCallAggregates` — pour le comptage | `backend/ringover-call-events.go:2002-2005` : commentaire d'auteur explicite — *« A Ringover call can fan out to several user channels. When a channel is present, falling back to raw_call_id would merge a new sibling into the first row and overwrite its provider identity. »* puis `return nil, nil` → une ligne neuve par canal.<br>`backend/ringover-call-events.go:2834-2845` : `upsertAppel` **n'écrit pas** `parent_appel_id` sur ces lignes sœurs (seul `:2929`, réservé au rappel manuel, le fait) — elles franchissent donc toutes le filtre `parent_appel_id = ''` du comptage (`backend/flexylead-app-dashboard-query.mjs:744,773`).<br>`backend/ringover-call-events.go:2676-2708` : après décrochage, `suppressMissedSiblingChannelsAfterAnswer` sort les sœurs de la file (`missed_pool_status = 'routed'`) **sans modifier leur `status`** — elles restent « manquées » pour tout compteur.<br>Contre-épreuve : la famille pilotage, elle, recolle les jambes via `interaction_id` (`backend/flexylead-app-pilotage-summary-query.mjs:3100-3115`, CTE `linked_legs`), que `queryCallAggregates` n'utilise pas. | Précision utile : le refus de regrouper est **documenté à l'insertion** (préserver l'identité fournisseur), et **non compensé au comptage** par le tableau de bord — alors que le pilotage, lui, le compense. |
32| **1.3** | « la colonne "uniques" du total ne compte que les appels venus de la publicité : elle ne peut pas, structurellement, égaler la somme des lignes en dessous. » | **VRAI** | `queryCallAggregates` (dans `flexylead-app-dashboard-query`) | `backend/flexylead-app-dashboard-query.mjs:848-849` : les lignes par commercial font `COUNT(DISTINCT opportunity_key)` sur la cohorte `scoped`.<br>`:858-859` : la ligne `__total__` va chercher `unique_received` / `unique_answered` dans `google_acquisition_metrics`.<br>`:788-804` : `google_candidates` n'admet que les appelants rattachés à une campagne `provider = 'google_ads'` et `mapping_status = 'matched'` (`:707-713`), **et** `:805-824` jamais vus dans l'historique. Deux restrictions que les lignes du dessous n'ont pas. | — |
33| **1.4** | « Trois correctifs existent déjà dans les sources. Ils réparent la définition de "manqué", la colonne du total, et le rattachement au bon commercial. Aucun n'est en service. » | **VRAI** | Correctif 1 : `missedCallSql` (fonction **nouvelle**, exportée) · Correctif 2 : `queryCallAggregates` · Correctif 3 : `reconcileConfirmedAIHandling`, `isUnattributedIVRAnswer`, `aiHandlingRepairable`, `confirmedAIHandlingFields` (quatre fonctions **nouvelles**) | **1** `functions/flexylead-app-dashboard-query.mjs:153-157` : `missedCallSql` teste `status` **OU** `issue` **OU** `motif` (là où le déployé s'arrête au premier champ non vide) ; appliquée en `:232,261,740,769`. Absente du déployé.<br>**2** `functions/flexylead-app-dashboard-query.mjs:866-867` : la ligne `__total__` devient `(SELECT COUNT(DISTINCT opportunity_key) FROM scoped)` — même cohorte que les lignes du dessous. Déployé : `backend/…:858-859`.<br>**3** `functions/ringover-call-events/main.go:625-637` : `isUnattributedIVRAnswer`, commentaire d'auteur — *« A commercial inherited from a ringing event is a target, not proof of pickup. Only the provider identity on this event can identify its actor. »* ; `:684-745` : `reconcileConfirmedAIHandling` et ses deux auxiliaires. Aucune de ces quatre fonctions n'existe dans `backend/ringover-call-events.go` (`grep -c 'reconcileConfirmedAIHandling\|isUnattributedIVRAnswer' backend/ringover-call-events.go` → `0`). | **À signaler dans le texte publié :** ces trois correctifs sont dans la **source de travail**, pas dans le déployé. La phrase le dit déjà (« Aucun n'est en service ») — elle est exacte. |
34| **2.0** | « Deux compteurs justes portent le même nom » (titre) | **VRAI** | `saleRevenue` et `reportSaleRevenue` (famille pilotage) | Voir 2.1 et 2.2. Les deux fonctions coexistent dans le même fichier déployé, à onze lignes d'écart. | — |
35| **2.1** | « Le pilotage suit la règle en vigueur à la date. » | **VRAI** | `reportSaleRevenue` · `remunerationRuleFor` · `remunerationRuleAmount` (famille pilotage, p. ex. `flexylead-app-pilotage-remuneration-query`) | `backend/flexylead-app-pilotage-remuneration-query.mjs:2177-2185` : commentaire d'auteur, puis `reportSaleRevenue` défini `:2182` — *« Financial reporting follows the rule effective on the sale date. Snapshots remain the immutable audit trail… but a newly effective remuneration grid must be visible immediately in Pilotage without rewriting historical sale rows (TKT-116). »* puis `reportSaleRevenue` renvoie la **règle** dès qu'elle existe, l'instantané seulement en repli.<br>`:2094-2108` : `remunerationRuleFor` filtre par `effectiveOn(rule, date)` ; `:2110-2116` `remunerationRuleAmount` recalcule le montant.<br>`:993-997` : la règle recalculée prime sur `sale.prime_montant`. Contraste avec `saleRevenue` (`:2171`), qui, lui, donne toujours la priorité à l'instantané. | — |
36| **2.2** | « La vente, elle, garde le montant figé au moment où elle a été conclue. » | **VRAI** | `resolveRevenueSnapshot` et `resolveRemunerationSnapshot` (dans `flexylead-create-sales`) · `buildSale` | `backend/flexylead-create-sales.mjs:695-701` : `buildSale` écrit `revenue_cents_snapshot`, `prime_montant`, `prime_devise`, `prime_rule_id` sur la ligne de vente.<br>`:733-762` : `resolveRemunerationSnapshot` (défini `:733`) choisit la règle par `effectiveOn(row, saleDate)` **au moment de la création**, et le montant retenu n'est plus jamais réévalué. | — |
37| **2.3** | « Ce ne sont pas deux compteurs faux, ce sont deux compteurs justes — un test du prestataire exige même qu'ils diffèrent. » | **VRAI** | test **`TKT-111/TKT-116`** portant sur `buildPilotageProduction` | `functions/flexylead-app-pilotage-query.test.mjs:2488` : `test("TKT-111/TKT-116 : la rémunération Télécom suit la règle offre + variante + date, pas l'ancien instantané", …)`.<br>`:2497` la vente porte `prime_montant: 1` (figé) ; `:2515` `assert.equal(profit.telephonyRemuneration, 63)` ; **`:2516` `assert.notEqual(profit.telephonyRemuneration, 1)`** — la divergence est une exigence écrite.<br>Deuxième occurrence : `:2453-2455`, commentaire — *« Simulate a stale snapshot created before the effective grid was corrected: Pilotage must use the rule effective on the sale date (TKT-116). »* | **À signaler :** le **test** n'existe que dans la source de travail ; le **comportement testé**, lui, est bien dans le déployé (`backend/…:2177-2184`, `:993-997`). Formulation sûre : « un test du prestataire, écrit mais non livré en production, exige même qu'ils diffèrent — le comportement, lui, est en service. » |
38| **2.4** | « Mais rien, à l'écran, ne dit lequel on regarde. » | **À BORNER — et à corriger** | `metricsResource` (famille commerciale) · `buildPilotageProduction` (famille pilotage) | Recherche : `grep -rIn "règle en vigueur\|instantané\|snapshot\|figé" src/routes/PilotageProductionRoute.tsx` → **aucun résultat**. Aucun libellé n'indique la règle appliquée.<br>Mais l'opposition réelle n'est pas celle que la phrase suggère : `backend/flexylead-app-pilotage-remuneration-query.mjs:755` `row.bonusCost += nonNegative(sale.prime_montant)` — les « Primes commerciales » du pilotage (`src/routes/PilotageProductionRoute.tsx:1363,1454,1509`) lisent le montant **figé**, exactement comme l'écran commercial (`backend/flexylead-app-commercial-metrics-query.mjs:732` ; `src/main.tsx:6960,11141`). Ces deux-là **sont d'accord**.<br>Le compteur qui diverge est `centreRemuneration` / `telephonyRemuneration`, recalculé. Or il s'alimente à la **même** table `offer_remuneration_rules` et au **même** champ `prime_montant` que la prime du commercial. | « Mais rien, à l'écran, ne dit lequel on regarde — et les deux chiffres puisent dans la même case, `prime_montant`, qui sert à la fois de prime du commercial et de tarif du centre. » |
39| **3.1** | « À onze endroits de l'interface, quand une donnée n'arrive pas, l'écran affiche zéro au lieu d'une erreur. » | **À BORNER — le chiffre est juste, la description est fausse** | `loadFullState` (dans `src/dashboard/api/client.ts`) | `src/dashboard/api/client.ts:1007-1019` : `const optional = new Set<StateTable>([…])` contient **exactement onze** tables — `reservations`, `commerciaux`, `commercial_numbers`, `commercial_groups`, `commercial_group_members`, `routing_endpoints`, `campaigns`, `commercial_objectives`, `stats_periods`, `offer_remuneration_rules`, `prospects`.<br>`:1023` : `return optional.has(table) ? promise.catch(() => []) : promise;`<br>Ce ne sont donc pas onze *endroits de l'interface*, ce sont **onze tables** déclarées facultatives dans **une seule fonction**. Parmi elles, `offer_remuneration_rules` et `stats_periods` — les barèmes et les périodes. | « Une seule fonction, `loadFullState`, déclare onze tables "facultatives" : si l'une d'elles ne répond pas, l'écran la remplace par une liste vide et n'affiche aucune erreur. Deux de ces onze tables sont les barèmes et les périodes de paie. » |
40| **3.2** | « Le serveur fait la même chose : trente-huit fois, dans dix fichiers — dont seize dans un seul. » | **À BORNER — chiffres non reproductibles** | famille des fonctions commerciales | Trois motifs de recherche ont été essayés sur `backend/`, aucun ne donne 38/10/16 : `catch` renvoyant une valeur vide sans `throw` → **119** occurrences brutes, **46** après déduplication des familles, dans 25 fichiers-sources ; `.catch(…) =>` valeur vide → **11** par fichier de la famille commerciale ; toute occurrence de `.catch(` → **17** par fichier de cette même famille, 34 fichiers.<br>Le « seize dans un seul » approche le chiffre de la famille commerciale (16 `.catch(…) =>`), mais ce fichier est déployé **23 fois** : « dix fichiers » est structurellement trompeur.<br>De plus, une part notable des `catch` recensés sont des **analyses JSON** (`jsonArray`, `jsonObject`, `arrayValue`), pas des échecs de chargement — les mélanger fausse le compte. | « Le serveur fait la même chose. Le cas le plus lourd est la fonction unique dont sont tirées les vingt-trois fonctions de l'espace commercial : elle contient seize conversions d'erreur en résultat vide. » (chiffre vérifiable : `grep -c "\.catch(" backend/flexylead-app-commercial-metrics-query.mjs` → 17, dont 16 en position de masquage) |
41| **3.3** | « Les fonctions de pilotage, elles, refusent de le faire. » | **VRAI, avec une réserve à porter dans le texte** | `callTables` (famille pilotage) | `backend/flexylead-app-pilotage-summary-query.mjs:4400,4411-4413` : `callTables` relance l'erreur (`throw error`) dès que le motif n'est pas un dépassement de file.<br>`:3018-3025` : `timedFactQuery` réémet l'erreur en la nommant (`throw new Error(...)`), sans jamais la convertir en résultat vide.<br>Réserve : le pilotage renvoie tout de même `[]` en six points — `:2792, 3955, 4057, 4112, 4151, 4283, 4319` — mais **uniquement** sur `not found | unknown table | does not exist | unresolved {placeholder}`, c'est-à-dire une table absente, jamais un échec de lecture. Et `:2611, 2622, 2633` sont des analyses JSON. | « Les fonctions de pilotage, elles, refusent de le faire : leur fonction de lecture, `callTables`, relance l'erreur. Elles ne tolèrent le vide que pour une table qui n'existe pas. » |
42| **3.4** | « Un correctif existe déjà dans les sources, et la production fait toujours l'ancien geste. » | **VRAI** | `missedCallsResource` | Déployé : `backend/flexylead-app-commercial-missed-calls-query.mjs:289` — `…SELECTS.assignments).catch(() => ({ rows: [], total: 0 }));` : si la file d'attente ne répond pas, le commercial voit **zéro appel manqué**.<br>Travail : `functions/flexylead-app-commercial-resource-query.mjs:285-286` — le `.catch` a disparu, remplacé par `if (!centreId || !aliases.length) throw new Error('Commercial queue scope missing');`.<br>Deux autres `.catch(() => ({ rows: [], total: 0 }))` subsistent en travail (`:414, :419`) : le correctif est **partiel**. | Ajouter : « Le correctif ne couvre qu'un des trois masquages de cette fonction. » |
43| **4.1** | « Quand une requête est trop lente, la production la relance — jusqu'à douze fois » (titre) | **VRAI** | `queryPartitionedPilotageInteractions` × `callTables` (famille pilotage) | `backend/flexylead-app-pilotage-summary-query.mjs:3429` : `for (let bucket = 0; bucket < 4; bucket++)` → **4** exécutions, chacune passant par `callTables` (`:3439`).<br>`:106` : `const TRANSIENT_READ_RETRIES = 2;` → 1 tentative + 2 reprises = **3** par morceau.<br>4 × 3 = **12**. Le chiffre est exact. | — |
44| **4.2** | « Le pilotage découpe ses grosses requêtes en quatre morceaux et réessaie deux fois en cas d'échec. » | **VRAI** | `queryPartitionedPilotageInteractions` (appelée depuis `querySinglePassAggregatedCallFacts`) · `callTables` | `backend/flexylead-app-pilotage-summary-query.mjs:3389-3391` (branchement), `:3414` (définition), `:3429-3439` (la boucle de 4 et l'appel), `:106` et `:4400-4419` (les 2 reprises). | — |
45| **4.3** | « Or la production compte le dépassement de délai parmi les échecs qui méritent une reprise. » | **VRAI** | `callTables` | `backend/flexylead-app-pilotage-summary-query.mjs:4412` : le motif de reprise est `/read_queue.*deadline exceeded|`**`context deadline exceeded`**`|deadline exceeded.*read_queue/i`. La branche du milieu ne mentionne pas `read_queue` : c'est le dépassement d'**exécution**.<br>Confirmation par la plateforme : `code/apteva-platform/apps/mcp/tables/types.go:330` (`max_query_ms`, exécution) et `:334` (`max_read_queue_ms`, attente) sont deux réglages distincts ; `apps/mcp/tables/schema_cache.go:211-212` n'étiquette `read_queue` que sur l'attente. Les deux étages sont bien séparés côté serveur, et le CRM les confond. | — |
46| **4.4** | « La source a retiré ce comportement, avec le commentaire de son auteur : "rejouer la même requête coûteuse multiplie la charge sans rien changer". » | **VRAI — mais c'est une traduction** | `callTables` · `queryReducedPilotageInteractions` (fonction **nouvelle**, en remplacement de `queryPartitionedPilotageInteractions`) | `functions/flexylead-app-pilotage-query.mjs:4394-4397` : commentaire d'auteur, **en anglais** — *« An execution deadline is not an admission/queue timeout. Re-running the same expensive SELECT multiplies load without changing its plan. »* ; la branche `context deadline exceeded` a disparu de la condition.<br>Le correctif agit sur **deux** leviers, pas un : `:3391` appelle désormais `queryReducedPilotageInteractions` (`:3414-3427`), **une seule requête** au lieu de quatre. `queryPartitionedPilotageInteractions` n'existe plus (`grep -c` → 0). Résultat : 1 exécution au pire cas contre 12. | « La source a retiré ce comportement — et, du même geste, le découpage en quatre : `queryPartitionedPilotageInteractions` cède la place à `queryReducedPilotageInteractions`, une seule requête. Le commentaire de l'auteur, traduit : "rejouer la même requête coûteuse multiplie la charge sans rien changer". » |
47| **4.5** | « Le dépassement de délai n'est donc pas seulement un symptôme de la surcharge : il en est aussi une cause. » | **VRAI comme déduction — niveau SUGGÉRÉ** | — | La chaîne causale est entièrement lisible dans le code (4.1 à 4.3). Ce qui manque : aucune mesure en production, aucun journal d'exécution. C'est un raisonnement, pas une observation. | Conserver la phrase, l'assortir du niveau « suggéré ». |
48| **5.1** | « Le logiciel contient un vérificateur qui cherche les incohérences : une opportunité attribuée deux fois, un rappel dont l'appel a disparu. » | **VRAI** | `flexylead-integrity-audit` · contrôles **`duplicate_active_opportunities`** et **`callback_reservations_without_call`** | `backend/flexylead-integrity-audit.mjs:9-18` : `duplicate_active_opportunities` — `GROUP BY active_opportunity_key HAVING COUNT(*) > 1` sur les affectations actives.<br>`:19-43` : `callback_reservations_without_call` — réservations de rappel confirmées dont l'`appel_id` ne résout aucune ligne d'`appels`.<br>Trois autres contrôles, déclarés aux lignes `:44`, `:64` et `:72` : `calls_with_broken_reservation_link`, `failed_qualification_commands`, `failed_followup_commands`. | — |
49| **5.2** | « Il est correctement écrit. » | **À BORNER** | `runCheck` | `backend/flexylead-integrity-audit.mjs:97-110` : en cas d'échec, `runCheck` renvoie `{ available: false, count: 0, rows: [] }` — **le même geste que le constat 3**, un échec converti en zéro. Le drapeau `degraded` (`:89`) le signale, mais `issue_count` (`:90`) reste faux.<br>Chacun des cinq contrôles est plafonné à `LIMIT 100` (`:17, :42, :62, :70, :78`) : le compte d'anomalies est **écrêté**, jamais total. | « Il est écrit avec soin, mais il ne compte que les cent premières anomalies de chaque type, et il rend zéro quand il échoue. » |
50| **5.3** | « Aucun écran ne le lance. » | **VRAI** | — | Recherche exhaustive, dépôt entier, y compris les bundles compilés : `grep -rIn "integrity\.audit\|integrity_audit\|integrityAudit" /Users/gregorydelaloeuf/Documents/scalewayxflexylead/code/flexylead-crm-0908 --exclude-dir=.git` → **10 résultats**, tous dans `backend/flexylead-app-command-dispatch.mjs:8`, `functions/flexylead-app-command-dispatch.mjs:8` et les deux scripts de 5.4. **Aucun** dans `src/`, `dist/`, `prebuilt-dist/`.<br>Recherche d'un déclenchement automatique : `ls config/jobs/` → six tâches (`call-routing-reconciliation`, `google-ads-cost-sync`, `ringover-audio-reconcile`, `ringover-presence-refresh`, `routing-performance-scores`, `work-routing`). **Aucune** ne vise `flexylead-integrity-audit`. `grep -n "cron\|schedule" scripts/application-api-manifest.mjs` → aucun résultat.<br>La fonction est pourtant bien déployée et routée : `scripts/application-api-manifest.mjs:48` ; `backend/flexylead-app-command-dispatch.mjs:8` — `'integrity.audit': 'flexylead-integrity-audit'`. | — |
51| **5.4** | « Deux programmes de vérification l'appellent, à la main, dont un qui vise la base de production. » | **VRAI** | `verify-production-reliability-contracts-20260824.mjs` et `verify-staging-action-contracts.mjs` | `scripts/verify-production-reliability-contracts-20260824.mjs:39` : `ok('integrity.audit', 'admin', 'integrity.audit', { limit: 25 })` ; `:5` importe `production-project-client.mjs` — c'est bien la production.<br>Lancement manuel : `:7-11` exige un répertoire en argument et la variable `CONFIRM_PRODUCTION_RELIABILITY_SMOKE=READ_ONLY_AND_INVALID_PAYLOADS`.<br>`scripts/verify-staging-action-contracts.mjs:24` : même sonde, sur la pré-production.<br>Point à retenir : la sonde est bornée à `limit: 25` — elle vérifie que le contrôle **répond**, pas ce qu'il trouve. | Ajouter : « et l'un comme l'autre ne lui demandent que vingt-cinq lignes : ils vérifient qu'il répond, pas ce qu'il trouve. » |
52| **C3** | Carte, étage 3, « La direction » : « Le mot "manqué" a trois définitions différentes selon l'écran, et les compteurs ne se recoupent jamais entre eux. » | **Première moitié VRAI · seconde moitié À BORNER** | `queryCallAggregates` · `metricsResource` · `queryAcquisitionFacts` | Première moitié : voir 1.1.<br>Seconde moitié : « jamais » est un absolu qui exigerait de comparer les chiffres réels ; aucune exécution n'a été faite. Ce qui est **prouvé**, c'est l'impossibilité structurelle de recoupement sur la colonne « uniques » de la ligne de total (voir 1.3), et l'écart de définition entre les trois fonctions (1.1). | « Le mot "manqué" a trois définitions différentes selon l'écran, et l'une des colonnes du total ne peut structurellement pas égaler la somme des lignes qu'elle chapeaute. » |
53| **C5** | Carte, étage 5, « Les statistiques et le coût d'acquisition » : « Ces chiffres viennent des compteurs de l'étage 3, ceux qui ne se recoupent pas. » | **FAUX** | `queryAcquisitionFacts` et `summarizePeriod` (dans `flexylead-app-acquisition-query`) | Le coût d'acquisition **ne réutilise aucun** compteur de l'étage 3. `backend/flexylead-app-acquisition-query.mjs:258,271` : `summarizePeriod` calcule `averageCost = spend / totals.newUnique`, et `newUnique` vient de `queryAcquisitionFacts` (`:172` — `SUM(is_new)`), qui construit sa propre cohorte : premier appel d'un numéro dans la période (`:152-163`, `ranked` puis `caller_rank = 1`), jamais vu auparavant (`:22-34`, `acquisitionNewCallerPredicate`, appliqué `:166`), et restreint aux campagnes publicitaires appariées (`:121`, jointure sur `mapped_sources`).<br>C'est une **quatrième** définition, indépendante de `queryCallAggregates` comme du pilotage. Le défaut est plus grave que ce que la carte annonce : le dénominateur du coût par contact n'est réconciliable avec **aucun** compteur d'appels affiché ailleurs dans le logiciel. | « **Défaut** — Ces chiffres n'ont pas de dénominateur commun avec ceux de l'étage 3 : le coût par contact se divise par un quatrième compteur d'appels, calculé pour lui seul. » |
54
55---
56
57## Ce qui a été cherché et n'a pas été trouvé (constats d'absence, avec la commande)
58
59Toutes les commandes ci-dessous ont été passées depuis `/Users/gregorydelaloeuf/Documents/scalewayxflexylead/code/flexylead-crm-0908`.
60
61| Affirmation d'absence | Commande | Résultat |
62|---|---|---|
63| Aucun écran ne lance le contrôle de cohérence | `grep -rIn "integrity\.audit\|integrity_audit\|integrityAudit" . --exclude-dir=.git` | 10 résultats, aucun sous `src/`, `dist/`, `prebuilt-dist/` |
64| Aucune tâche planifiée ne le déclenche | `ls config/jobs/` puis lecture des six fichiers ; `grep -n "cron\|schedule\|trigger" scripts/application-api-manifest.mjs` | six tâches, aucune ne vise `flexylead-integrity-audit` ; aucun ordonnancement dans le manifeste |
65| Les correctifs de `ringover-call-events` ne sont pas déployés | `grep -c "reconcileConfirmedAIHandling\|isUnattributedIVRAnswer\|aiHandlingRepairable\|confirmedAIHandlingFields" backend/ringover-call-events.go` | `0` (contre 4 fonctions définies dans `functions/ringover-call-events/main.go`) |
66| `missedCallSql` n'est pas déployée | `grep -c "missedCallSql" backend/flexylead-app-dashboard-query.mjs` | `0` |
67| Le découpage en quatre n'a pas été retiré en production | `grep -c "queryPartitionedPilotageInteractions" backend/flexylead-app-pilotage-summary-query.mjs` puis `functions/flexylead-app-pilotage-query.mjs` | `2` déployé, `0` en travail |
68| Aucun libellé d'écran n'indique la règle de rémunération appliquée | `grep -rIn "règle en vigueur\|instantané\|snapshot\|figé" src/routes/PilotageProductionRoute.tsx` | aucun résultat |
69| Le déployé n'a pas bougé entre les deux instantanés | `md5` comparé fichier à fichier sur les cinq fichiers porteurs de constats | identiques |
70
71## Fonctions nommables sans risque dans le texte publié
72
73Ces noms sont uniques dans le corpus déployé et ne prêtent pas à confusion :
74
75`queryCallAggregates` · `queryDashboardCallAndEnergyAggregates` · `confirmedBusinessAnswerSql` · `missedCallSql` · `queryAcquisitionFacts` · `summarizePeriod` · `acquisitionNewCallerPredicate` · `findExistingAppel` · `upsertAppel` · `suppressMissedSiblingChannelsAfterAnswer` · `closeOpenSiblingChannelAppels` · `isUnattributedIVRAnswer` · `reconcileConfirmedAIHandling` · `saleRevenue` · `reportSaleRevenue` · `resolveRevenueSnapshot` · `resolveRemunerationSnapshot` · `buildSale` · `remunerationRuleFor` · `remunerationRuleAmount` · `buildPilotageProduction` · `callTables` · `queryPartitionedPilotageInteractions` · `queryReducedPilotageInteractions` · `loadFullState` · `missedCallsResource` · `metricsResource` · `runCheck` · `flexylead-integrity-audit`.
76
77**À ne pas nommer isolément** : toute fonction de la famille commerciale ou de la famille pilotage citée comme *fichier* — dire « la famille des fonctions de pilotage » ou « la fonction unique dont sont tirées les vingt-trois fonctions de l'espace commercial ». En revanche, désigner un **point d'entrée** déployé par son nom (`flexylead-app-pilotage-remuneration-query`, `flexylead-app-commercial-metrics-query`) est exact : ce sont bien des fonctions distinctes en production.
78