From c5f25684f78493227597a60770d383806c44d93b Mon Sep 17 00:00:00 2001 From: Nabil Derouiche Date: Mon, 10 Aug 2026 17:24:17 +0100 Subject: [PATCH] hermes-tt: 4 bugs extraction OWA (A+B corriges, C+D diagnostiques) A - glyphe de zone privee Unicode seul en ligne 0 de raw_text decalant tout le decoupage positionnel : sender, subject ET date faux ensemble. 295/399 notes avaient un received_date qui n'est pas une date. 11 points de code PUA distincts recenses sur 40 dossiers, dont 7 hors de la plage Fluent UI habituelle -> retenir U+E000-U+F8FF. Correctif : nettoyage en amont du decoupage, date par motif, normalisation ISO 8601 au jour, message_id sur champs normalises + conversation_id comme discriminant. Corrige aussi checkpoint.last_processed_date (meme source). B - search_folder() ne transmettait pas limit a /search -> plafond serveur silencieux a 20 items, dossier marque done comme s'il etait complet. C - dossiers hors Reception inatteignables : 3 chemins de navigation, 3 defauts deja resolus ailleurs dans le meme fichier. Diagnostique, non corrige. D - NON RESOLU, BLOQUANT BACKFILL : /search renvoie le contenu de la Boite de reception pour certains folder_path, en HTTP 200. Invalide la methode d'audit de completude et remet en question le classement des 399 notes existantes. + 3 regles generales dans PROTOCOL-INFRA.md (glyphe/emoji cassant tout matching de texte naif sur OWA ; valeur par defaut serveur non transmise = troncature silencieuse ; verifier la portee d'un resultat scope, pas seulement son code HTTP). --- _INDEX.md | 1 + common/PROTOCOL-INFRA.md | 58 ++++ ...ora-notes-tt-parsing-owa-pua-10-08-2026.md | 249 ++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 hermes-tt/nyora-notes-tt-parsing-owa-pua-10-08-2026.md diff --git a/_INDEX.md b/_INDEX.md index 2c6a37c..b297fe3 100644 --- a/_INDEX.md +++ b/_INDEX.md @@ -100,6 +100,7 @@ | [16 dossiers RAG error — CORRIGE : clic d'expansion non idempotent go_to_folder_path (pas un etat de session degrade), + 2 bugs annexes /folders inner_text() et absence de verrou page partagee, verifie 22/22](hermes-tt/mail-o365-toggle-expand-non-idempotent-fix.md) | 2026-08-02 | mail, o365, owa, playwright, hermes-mail-browser, nyora-notes-tt, rag, checkpoint, error, race-condition, fix | | [onedrive-sync -- doublons non recatalogues (is_duplicate_of, pas un bug) + extensions inconvertibles ajoutees a UNCONVERTIBLE_EXT, verifie bad_ext_still_present=0](hermes-tt/nyora-notes-tt-onedrive-sync-doublons-extensions.md) | 2026-08-06 | nyora-notes-tt, onedrive-sync, catalog, doublons, extensions | | [SharePoint delta-sync (hermes-mail-browser -> archive locale) : mise en place complète](hermes-tt/sharepoint-delta-sync.md) | 2026-08-06 | sharepoint, sync, hermes-mail-browser, n8n, onedrive | +| [4 bugs extraction OWA — A+B CORRIGES (glyphe zone privée U+E000-U+F8FF décalant le découpage positionnel, 295/399 dates corrompues ; plafond /search silencieux à 20) · C dossiers hors Réception inatteignables · **D portée /search non respectée = BLOQUANT backfill**](hermes-tt/nyora-notes-tt-parsing-owa-pua-10-08-2026.md) | 2026-08-10 | mail, o365, owa, playwright, hermes-mail-browser, nyora-notes-tt, rag, parsing, unicode, pua, message-id, troncature, scope, fix | ## hermes-nyora diff --git a/common/PROTOCOL-INFRA.md b/common/PROTOCOL-INFRA.md index c067be1..5153550 100644 --- a/common/PROTOCOL-INFRA.md +++ b/common/PROTOCOL-INFRA.md @@ -240,3 +240,61 @@ est vide de facon inattendue, retry UNIQUE apres `time.sleep(4)` avant d'abandon backoff). Piege de parsing associe : `attrs` (string pipe-delimitee) reinjecte `aria-label` en fin de chaine (texte libre non echappe, `|` et sauts de ligne possibles) -> NE JAMAIS `split('|')` global ; extraire par regex ciblee (`data-convid=([^|]+)`, base64 sans `|`). Detail : meme runbook ci-dessus. + +## REGLE -- OWA : un glyphe d'icone (zone privee Unicode) ou un emoji casse tout matching de texte naif (2026-08-10) + +Le texte rendu par OWA est fait pour l'oeil, pas pour servir de structure de donnees. OWA insere des +glyphes d'icone issus de la ZONE PRIVEE Unicode (lu/non-lu, importance, piece jointe) directement dans +le texte scrape -- et les libelles de dossiers portent des emoji. Ces caracteres ne sont PAS des +espaces : `.strip()` ne les retire pas et ils survivent a un filtre `if l.strip()`. + +Le meme phenomene a maintenant casse TROIS surfaces differentes : +1. Parsing d'une ligne de message (`_parse_item`, nyora-notes-tt) : un glyphe seul sur la ligne 0 de + `raw_text`, present sur une partie seulement des messages, decale le decoupage positionnel d'un + cran -- expediteur, objet ET date faux ENSEMBLE. Resultat mesure : 295/399 notes avec un + `received_date` qui n'est pas une date (74%), dont 90 contenant un glyphe. +2. Clic sur le volet dossiers (`resolve_folder_treeitem`, hermes-mail-browser) : `inner_text()` peut + renvoyer vide ou reduit au seul glyphe d'icone -> aucun dossier ne matche. +3. Resolution de chemin de dossier (deja documente le 30-31/07/2026). + +PLAGE A RETENIR : la PUA COMPLETE U+E000-U+F8FF. La plage "Fluent UI" habituelle (U+E700-U+ECFF) est +INSUFFISANTE -- `U+E1B7` a ete observe en production hors de cette plage, a cote de `U+E73E`. + +REFLEXES : +- Lire des ATTRIBUTS STRUCTURES (`data-folder-name`, `data-convid`, `aria-*`) plutot que du texte rendu. +- Nettoyer la plage PUA AVANT tout decoupage, jamais champ par champ en aval (c'est un decalage + d'indice unique qui fausse tous les champs d'un coup). +- Ne jamais lire une donnee par POSITION dans du texte rendu : chercher par MOTIF, et prevoir une + sentinelle explicite si rien ne matche -- ne jamais retomber sur une ligne arbitraire. +Detail : hermes-tt/nyora-notes-tt-parsing-owa-pua-10-08-2026.md. + +## REGLE -- une valeur par defaut cote serveur non transmise = troncature silencieuse (2026-08-10) + +`hermes_mail_client.search_folder()` n'a jamais transmis `limit` a `/search` : il l'appliquait en +tranche cliente (`[:limit]`) sur une reponse DEJA tronquee. `hermes-mail-browser` plafonne `/search` +a 20 items par defaut, SANS erreur ni avertissement -- le dossier passe ensuite en statut `done` +comme s'il avait ete traite entierement. Temoin `00 2026|Veille` : 14 notes + 6 doublons = exactement +20 en base pour 37 messages reels. + +REFLEXE : tout plafond cote serveur doit etre TRANSMIS EXPLICITEMENT, jamais laisse a sa valeur par +defaut. Une troncature silencieuse est indetectable en aval -- corollaire : quand un compteur colle +pile a une valeur ronde (20, 50, 100), suspecter un plafond avant de conclure que la source est vide. +Detail : meme runbook ci-dessus. + +## REGLE -- verifier la PORTEE d'un resultat scope, pas seulement son code HTTP (2026-08-10) + +`GET /search?folder_path=X` de hermes-mail-browser renvoie parfois le contenu de la BOITE DE +RECEPTION au lieu du dossier demande -- en HTTP 200, sans aucune erreur. Deux chemins differents +renvoyaient 77 items dont 63 communs, toutes les lignes portant le libelle "Boite de reception". +Deterministe et lie au chemin (un dossier connu-bon reteste apres un connu-mauvais redonne le bon +resultat) : ce n'est ni une derive d'etat ni de la contention. Cause probable : le segment est +trouve dans l'arbre mais le clic ne change pas reellement de dossier, et la recherche s'execute +alors dans la portee heritee -- `go_to_folder_path` ne leve une 404 que si un segment est +INTROUVABLE, jamais si la navigation echoue silencieusement apres un clic accepte. + +REFLEXE : un 200 ne prouve pas que la portee demandee a ete respectee. Quand une API navigue avant +de lire, verifier dans la REPONSE une marque de la portee obtenue. Ici c'est gratuit : la derniere +ligne de `raw_text` porte le libelle du dossier du message -- s'il ne correspond pas au dossier +demande, la navigation a echoue. Corollaire : un audit de completude bati sur une source non +verifiee produit des chiffres qui ont l'air precis et ne veulent rien dire. +Detail : hermes-tt/nyora-notes-tt-parsing-owa-pua-10-08-2026.md (bug D). diff --git a/hermes-tt/nyora-notes-tt-parsing-owa-pua-10-08-2026.md b/hermes-tt/nyora-notes-tt-parsing-owa-pua-10-08-2026.md new file mode 100644 index 0000000..ccf66d3 --- /dev/null +++ b/hermes-tt/nyora-notes-tt-parsing-owa-pua-10-08-2026.md @@ -0,0 +1,249 @@ +# nyora-notes-tt : 4 bugs de la chaîne d'extraction OWA — glyphe de zone privée, plafond /search silencieux, dossiers hors Réception inatteignables, portée /search non respectée + +**Date** : 10/08/2026 +**Instance** : hermes-tt +**Statut** : A et B CORRIGÉS + déployés · C diagnostiqué (correctif non appliqué) · **D NON RÉSOLU — bloquant backfill** +**Fichiers** : `/volume1/docker/nyora-notes-tt/hermes_mail_client.py` (`_parse_item`, `search_folder`) · `/volume1/docker/hermes-mail-browser/app/main.py` (bug C, non modifié) + +Quatre défauts distincts de la même chaîne d'extraction, découverts ensemble. Ils partagent une +racine commune côté OWA : **du texte rendu pour l'œil humain, utilisé comme structure de données**. + +--- + +## Bug A — un glyphe de zone privée Unicode décale tout le découpage positionnel + +### Symptôme +`received_date` contenait des objets de mail, des noms d'expéditeur, ou un caractère invisible. +Mesure sur la base au moment du diagnostic : + +| Mesure | Résultat | +|---|---| +| `received_date` ne correspondant à **aucun** format de date | **295 / 399 (74 %)** | +| dont contenant un caractère de zone privée | 90 | +| Groupes (subject, sender, folder) portant des `message_id` multiples | 52 | +| `checkpoint.last_processed_date` corrompu | oui (même source) | + +### Cause +`_parse_item` découpait `raw_text` par position : `lines[0]`=expéditeur, `lines[1]`=objet, +`lines[2]`=date. Or OWA insère **un glyphe d'icône seul sur sa propre ligne, en tête de +`raw_text`, sur une partie seulement des messages** (état lu/non-lu, importance, pièce jointe). + +Ce caractère n'est **pas un caractère d'espacement** : `.strip()` ne le retire pas, et la ligne +survit au filtre `if l.strip()`. Tout est décalé d'un cran — expéditeur, objet **et** date +deviennent faux **ensemble**. + +Preuve, même dossier, même expéditeur, deux messages consécutifs : + +``` +idx 6 : 'Dr. Nexum Veille\n🔬 Veille IA…\nSam 25/07\n…' → lines[2] = date OK +idx 7 : '\nDr. Nexum Veille\n🔬 Veille IA…\nVen 24/07\n…' → lines[2] = objet FAUX +``` + +⚠️ **La plage Unicode « Fluent UI » habituelle (U+E700–U+ECFF) est INSUFFISANTE.** +Recensement sur 40 dossiers réels — **11 points de code distincts**, dont 7 hors de cette plage : + +| Point de code | Occurrences | Dans U+E700–U+ECFF ? | +|---|---|---| +| `U+E73E` | 1095 | oui | +| `U+E1B7` | 612 | **non** | +| `U+E135` | 61 | **non** | +| `U+E0FF` | 31 | **non** | +| `U+EA4C` | 24 | oui | +| `U+E7B9` | 9 | oui | +| `U+E350` | 9 | **non** | +| `U+E89E` | 3 | oui | +| `U+E340` / `U+E0B1` / `U+E45A` | 2 / 2 / 1 | **non** | + +Coder en dur un ou deux points de code ne tient pas. Retenir la **PUA complète : U+E000–U+F8FF**. + +### Cause aggravante : OWA réécrit la date du même message au fil du temps + +Trois formats, choisis selon l'ancienneté du message : + +| Format | Exemple | Quand | +|---|---|---| +| Heure seule | `09:01` | message du jour | +| Jour + jj/mm | `Sam 25/07` | récent (année implicite) | +| Date complète | `09/07/2026` | ancien | + +Comme `message_id = sha256(subject|date|sender)`, **le même message re-scrapé à deux moments +produisait deux identifiants** → doublon garanti dès qu'une synchronisation récurrente repasse +sur un dossier. Le glyphe faisait la même chose, en pire : sa simple apparition changeait l'id. + +### Correctif +Ordre imposé, les trois étapes dépendent l'une de l'autre : + +1. **Nettoyage en amont du découpage** — écarter les lignes entièrement composées de caractères + PUA, puis retirer les glyphes résiduels *à l'intérieur* des lignes conservées. Corriger + `sender`/`subject`/`date` séparément en aval ne marche pas : c'est le même décalage d'indice + qui les fausse tous les trois. +2. **Date trouvée par reconnaissance de motif, pas par position** — balayage des lignes à partir + de l'indice 1, exclusion de la dernière ligne (libellé de dossier). Sentinelle explicite + `date_inconnue` si rien ne matche : ne **jamais** retomber sur une ligne arbitraire, c'est + exactement ce qui a rempli 74 % de la base. +3. **Normalisation ISO 8601 à la granularité du JOUR** (`normaliser_date_owa`). Volontairement + pas l'heure : c'est ce qui rend les trois formats convergents. Année absente → occurrence + **passée** la plus récente, jamais dans le futur (un message du 28/12 lu le 3 janvier + appartient à l'année précédente). +4. **`message_id` = hash des champs normalisés**, `conversation_id` inclus comme discriminant. + +### Piège découvert en corrigeant : la granularité au jour fait perdre un discriminant +Ramener la date au jour fait collisionner deux messages distincts de même objet, même expéditeur +et même jour (cas réel : newsletter émise plusieurs fois par jour sous un objet identique). Le +second aurait été **silencieusement sauté** comme déjà traité. + +Parade : inclure `conversation_id` (`data-convid`, ConversationId Exchange natif et stable, cf +`nyora-notes-tt-message-id-elem-id-volatile-08-08-2026.md`) dans le hash. Mesure sur les 37 +lignes de `00 2026|Veille` : + +| Composition du hash | Identifiants distincts | +|---|---| +| subject + date_iso + sender | 25 / 37 | +| **+ conversation_id** | **34 / 37** ← 34 messages réellement distincts | + +Les 3 lignes restantes sont le **même message capturé deux fois** par la liste virtualisée : +une paire aux `raw_text` identiques, deux paires ne différant que par le glyphe en tête. Les +fusionner est le comportement correct — et c'est la démonstration directe du fix. + +### Vérification (avant / après, sur données réelles) + +``` +idx3 (sans PUA) AVANT sender='Dr. Nexum Veille' date='Ven 24/07' id=msg_ad9e71e655030f3a +idx7 (avec PUA) AVANT sender='' date='🔬 Veille IA Dr. Nexum…' id=msg_5fefeb296ba7ba15 + → DEUX ids pour UN message + +idx3 APRÈS sender='Dr. Nexum Veille' date='2026-07-24' id=msg_5711010f8be85848 +idx7 APRÈS sender='Dr. Nexum Veille' date='2026-07-24' id=msg_5711010f8be85848 + → id stable, doublon évité +``` + +Sur les 37 lignes : **37/37 dates ISO reconnues, 0 caractère PUA résiduel**. Les dates se +recoupent avec le contenu (`Saturday 25 july 2026` dans l'objet ↔ `received_date=2026-07-25`). + +`checkpoint.last_processed_date` est corrigé **par la même modification** : `pipeline_processor.py` +consomme `item["received_date"]` aux trois emplacements d'écriture (l.376-377, 399-401, 417-418) +et ne recompose jamais de date ailleurs — vérifié. + +--- + +## Bug B — `/search` plafonne silencieusement à 20 items + +`search_folder()` n'a **jamais transmis** `limit` à l'appel HTTP : le paramètre n'était appliqué +qu'en tranche cliente (`[:limit]`) sur une réponse **déjà tronquée par le serveur**. +`hermes-mail-browser` plafonne `/search` à `limit=20` par défaut — **sans erreur ni +avertissement**. Le dossier passe ensuite en statut `done` comme s'il avait été traité en entier. + +Cas témoin `00 2026|Veille` : 14 notes + 6 doublons vectoriels = **exactement 20** en base, pour +**37 messages réels** (vérifié avec `limit=60`). + +```python +# AVANT — limit perdu en route +params = {"q": query, "folder_path": folder_path} +... +return processed_items[:limit] # tranche sur une réponse déjà tronquée + +# APRÈS +params = {"q": query, "folder_path": folder_path, "limit": limit} # DEFAULT_SEARCH_LIMIT = 200 +``` + +**Leçon** : une troncature silencieuse est indétectable en aval. Tout plafond côté serveur doit +être transmis explicitement, jamais laissé à sa valeur par défaut. + +--- + +## Bug C — « Éléments envoyés » (et tout dossier hors Réception) inatteignable + +Diagnostiqué, **non corrigé** (hors périmètre de la session). Ni limitation OWA ni problème de +permission : **trois** chemins de navigation, chacun avec un défaut déjà résolu ailleurs dans le +même fichier `main.py`. + +| Paramètre | Mécanisme | Défaut | Constaté | +|---|---|---|---| +| `folder_path=` | `go_to_folder_path()` | **Ancre en dur sur Boîte de réception** (`cur = inbox_candidates[0]`, l.325-332) — ne peut jamais atteindre un frère de niveau 2 | 404 « introuvable sous 'boîte de réception' » | +| `folder=` | `resolve_folder_treeitem()` (l.207) | Utilise `inner_text()`, que le docstring de `snapshot_folders` (l.222-232) déclare lui-même non fiable — « peut laisser inner_text() vide ou réduit à un glyphe d'icône » | 404 sur `envoy`, `éléments envoyés` | +| `folder_index=` | `go_to_folder()` (l.416-423) | `Locator.click()` natif, exige la visibilité réelle — échoue sur les items masqués par Fluent UI, exactement le problème résolu l.304-311 par `page.evaluate("el => el.click()")` | HTTP 500, 11 retries « element is not visible » | + +**Correctif proposé** (une ligne) : dans `go_to_folder_path`, résoudre le premier segment parmi +les enfants de niveau 2 de la racine du compte, avec repli sur la Boîte de réception pour la +rétrocompatibilité. Toute la machinerie fiable existe déjà (`snapshot_folders` + clic JS). Coût +réel : rebuild et retest Playwright d'un **second** container. + +--- + +## Bug D — `/search` renvoie le contenu de la Boîte de réception pour certains `folder_path`, en HTTP 200 + +**Découvert en auditant le bug B. NON RÉSOLU — bloquant pour tout backfill.** + +### Symptôme +Deux `folder_path` différents renvoient ~77 items dont 63 en commun, et **chaque ligne porte le +libellé de dossier « Boîte de réception »** au lieu du dossier ciblé. Aucune erreur : HTTP 200. + +### Test discriminant (chemin-spécifique, pas dérive d'état) +Trois appels consécutifs, navigateur libre de toute autre charge : + +| Appel | `folder_path` | Items | Libellé porté par les lignes | +|---|---|---|---| +| A | `00 2026\|⏱️ suivi` | 81 | `⏱️ Suivi` — **correctement scopé** | +| B | `00 2026\|Relance marchés RLA` | 76 | `Boîte de réception` — **NON scopé** | +| C | `00 2026\|⏱️ suivi` (re-test) | 81 | `⏱️ Suivi` — reproductible | + +C reproduit A à l'identique après B : le défaut est **déterministe et lié au chemin**, ce n'est +pas une dérive d'état de l'UI ni un effet de contention. + +### Hypothèse écartée +« Chemin stocké sans emoji vs libellé réel avec emoji » : `01 2025|Consommables 2025` a pour +libellé réel `consommables 2025`, **sans emoji**, et échoue quand même. Par ailleurs +`go_to_folder_path` matche en sous-chaîne insensible à la casse — un emoji dans le libellé ne +peut pas faire échouer le match à lui seul. + +### Piste la plus probable (à confirmer) +Le segment est bien *trouvé* dans l'arbre, mais le **clic ne change pas réellement de dossier** ; +la page reste sur la Boîte de réception et `perform_search` s'exécute donc dans sa portée. Ça +expliquerait le HTTP 200 sans erreur : `go_to_folder_path` ne lève une 404 que si un segment est +introuvable, jamais si la navigation échoue **silencieusement** après un clic accepté. + +### Conséquences (importantes) +1. **La méthode de l'audit de complétude est invalidée** pour les chemins touchés : le « compte + réel » mesuré est celui de la Boîte de réception, pas celui du dossier. Tout chiffre de + « messages manquants » calculé ainsi est un artefact. +2. **Intégrité des 399 notes existantes à réexaminer** : le pipeline ingère via ce même appel + (`search_folder` → `/search?q=*`). Des messages de la Boîte de réception ont pu être classés + sous des `folder_path` arbitraires. La dédup `message_id` étant **globale** (et non par + dossier), le premier dossier scanné absorbe les messages et les suivants ressortent quasi + vides — ce qui correspond au profil observé (beaucoup de dossiers à 0-3 notes). +3. **Bloquant pour le backfill** : ingérer à grande échelle avec une portée fausse polluerait la + base massivement. + +### Contrôle de portée à systématiser +La dernière ligne de `raw_text` porte le libellé du dossier du message. C'est un **contrôle de +portée gratuit** : après un `/search` scopé, si ce libellé ne correspond pas au dossier demandé, +la navigation a échoué. À câbler dans `search_folder` avant toute ingestion. + +--- + +## La leçon commune aux quatre bugs + +Le glyphe qui casse `inner_text()` sur le volet dossiers (bug C, déjà documenté le 30-31/07/2026 +dans `mail-o365-folder-path-disambiguation.md`) et celui qui décale `raw_text` sur les lignes de +messages (bug A) sont **le même phénomène**, jamais généralisé jusqu'ici. + +> Sur OWA, un glyphe d'icône ou un emoji casse tout matching de texte naïf — qu'il soit +> positionnel ou par `inner_text()`. Lire des **attributs structurés** +> (`data-folder-name`, `data-convid`, `aria-*`) plutôt que du texte rendu, et nettoyer la plage +> PUA U+E000–U+F8FF avant tout découpage. + +Ajouté à `PROTOCOL-INFRA.md` comme leçon générale (trois surfaces touchées : parsing de ligne de +message, clic sur le volet dossiers, résolution de chemin). + +## Procédure de déploiement (rappel) +Le code est **cuit dans l'image** (`build: .`), aucun hot-reload : +```bash +cd /volume1/docker/nyora-notes-tt +/usr/local/bin/docker compose build --no-cache +/usr/local/bin/docker compose up -d --force-recreate # jamais `up -d` seul +``` +Contrôle post-déploiement (les deux champs de date) : +```sql +SELECT DISTINCT received_date FROM notes; -- que de l'ISO ou la sentinelle +SELECT DISTINCT last_processed_date FROM checkpoint; +```