---
title: "Des flux aux fils nestés : anti-doublon inter-sources sur la Carte"
subtitle: "Comment les FlowItem deviennent projet → session → items, pourquoi un doublon fusionné n’est pas une pénalité, et ce que 279 tests ont verrouillé"
date: 2026-09-30
time: "20:00"
author: VFG / Fil (Chronowflow)
publishedBy: code
status: pub
tags: ["fil", "chronowflow", "devblog", "carte", "fils", "dedup", "flux"]
---

L’attente « voir des fils nestés se créer automatiquement » **ne pouvait pas être satisfaite** tant que trois blocages structurels restaient en place. Ce billet raconte le diagnostic, les décisions explicites du lot du 15 septembre 2026, et les preuves réellement exécutées — y compris ce qui a été volontairement sauté.

## Trois blocages vérifiés dans le code

1. **Les adaptateurs Chronos / Pulse / Telegram repliaient** au lieu de construire un arbre : un tableau d’**un seul** nœud plat (`parentId: null`), body du type « N tours · M outils ». Seul le chemin blog imbriquait déjà.
2. **Le pipeline Flux n’atteignait pas la Carte.** La mindmap était alimentée exclusivement par `AgentGrid.onSnapshot → buildFilPatchFromChronos`. Les onglets Flux / Journal voyaient les items ; la Carte non.
3. **L’anti-doublon était partiel.** `dedupeRelayedActs` couvre le *même* enregistrement relayé deux fois. `ctx.duplicateNearby` (pénalité `-10`) n’avait **aucun appelant** en production — code mort. Correction d’errata du handoff : il n’est pas vrai qu’il n’existait « aucun » dédoublonnage ; il ne couvrait simplement pas « même travail, deux sources, pas d’id commun ».

## Décisions (aucune par défaut)

| Décision | Pourquoi |
| --- | --- |
| Espace d’ids **`fil:flow:`** | Éviter le clignotement avec le legacy `fil:event:` / `fil:agent:` (deux `parentId` concurrents) |
| **Zéro** pénalité de score sur un doublon fusionné | Vu par deux sources = mieux attesté, pas suspect ; `-10` reste pour `duplicateNearby` |
| Clustering **ancré** (fenêtre 5 min depuis la première ancre) | Le chaînage aurait fusionné une heure de titres récurrents |
| Priorité totale `chronos > git > pulse > telegram > blog` | Sans ordre total, l’item survivant clignote entre polls |
| Session = représentant le plus récent ; enfants = le reste | Évite le même titre parent *et* enfant |
| Parents et enfants **émis ensemble** | `flattenTree` part de `childrenOf(null,…)` : parent absent = nœud invisible |
| Fusion **visible** dans le bandeau | Une suppression silencieuse est exactement ce qu’on refuse |

Imbrication volontaire à **deux niveaux** : `projet → session → item`. Les tours d’une session restent repliés dans le `body` via `collapseChronosTurns` (non modifié dans ce lot).

## Livrables

| Module | Rôle |
| --- | --- |
| `src/lib/flows/dedup.ts` | Anti-doublon titre normalisé + fenêtre |
| `src/lib/flows/to-nodes.ts` | Items → arbre `projet → session → items` |
| `src/components/flow-map-sync.tsx` | Pont Flux → Carte |
| `capture.ts` + `/api/flows` | Dédoublonnage ; `mergedDuplicates` dans le bandeau santé |
| Tests | `dedupe.test.ts`, `flows-to-nodes.test.ts` (+20) |

Prérequis découvert : imports relatifs **sans extension** dans `src/lib` cassaient `node --experimental-strip-types` (`ERR_MODULE_NOT_FOUND`). Next tolérait ; les tests non. Corrigé dans le même lot — règle pour la suite : toujours `.ts` / `.tsx` dans un import relatif de `src/lib`.

## Limites honnêtes

- Ponts Inbox historiques (`fil:pulse:` / `fil:telegram:` / `fil:blog:`) restent hors Carte ; leurs items y arrivent via le **nouveau** chemin Flux. **Deux représentations** de Pulse (Inbox + Carte) coexistent — à trancher plus tard.
- Log console `[FlowMapSync]` **laissé volontairement** (demande utilisateur) ; à retirer après stabilisation.
- Alerte profondeur Carte (`DEPTH_WARN` 4 / `CRITICAL` 5) : le lot ajoute jusqu’à 2 niveaux sur des données auparavant plates — l’alerte s’allumera plus souvent ; seuils à arbitrer.

### Preuve (handoff fils nestés)

| Contrôle | Résultat déclaré |
| --- | --- |
| `npx tsc --noEmit` | exit **0** |
| `npx eslint src tests` | **0** erreur / **0** warning |
| `npm test` | **279/279** PASS (259 → +20) |
| `npm run build` | PASS |
| `/api/flows?days=7` | 200 · 19 items · `mergedDuplicates: 4` |
| Navigateur (bandeau) | `flux 3/3` · chronos 22 · pulse 1 · git 26 · **4 doublons fusionnés** |
| Navigateur (Carte) | branche projet → session · 69 nœuds observés |
| Console | `[FlowMapSync] 45 items → 56 nœuds, 10 racines, 18 sessions` |

### Non exécuté (volontaire dans ce lot)

- `tests/capture.browser.mjs` **non rejoué** (demande « skip all full test »).
- Mesure de performance non faite.
- Arbitrage seuils de profondeur / retrait du log / fusion des deux représentations Pulse — hors périmètre.

## Une phrase pour la suite

La Carte ne « invente » plus de branches : elle **matérialise** les flux déjà scorés, sans masquer les fusions — la prochaine irritation visible est la profondeur, pas l’absence de fils.

---

*Sources : `handoffs/2026-09-15-handoff-fils-nestes-antidoublon.md`, `STATUS.md`, `DERNIERE-INFO.md`. Miroir : `Chronowflow/docs/devblog/`.*