# Lead Pipeline — CLAUDE.md

> Spec de référence pour le pipeline de génération de leads Python.
> Phase 1 : Discovery + Enrichment + Scoring + Baserow Sync.

---

## 1. Objectif

CLI Python qui prend une **niche** + **région** en entrée, trouve des prospects via Firecrawl, les enrichit (scrape + PageSpeed), les score, et pousse les leads qualifiés dans **Baserow**.

SQLite local uniquement pour le **dedup** (éviter de rescraper les mêmes domaines).

---

## 2. Stack technique

| Outil | Usage |
|---|---|
| **Python 3.12+** | Runtime (via `mise`) |
| **uv** | Package manager (jamais pip) |
| **typer + rich** | CLI + affichage console |
| **httpx** | HTTP async (Firecrawl, PageSpeed, Baserow) |
| **pydantic + pydantic-settings** | Config, validation, env vars |
| **aiosqlite** | SQLite async (dedup) |
| **structlog** | Logs structurés (JSON fichier, rich console) |
| **tenacity** | Retry avec backoff exponentiel |
| **pyyaml** | Chargement config niches |

### Commandes obligatoires

```bash
uv add <pkg>        # Ajouter une dépendance
uv run <cmd>        # Exécuter (jamais python directement)
uv pip install      # Installer (jamais pip install)
```

---

## 3. Architecture — Flow pipeline

```
0. CONFIG
   niches.yaml → queries + régions
         │
         ▼
1. DISCOVERY
   Firecrawl POST /v1/search
   → Liste URLs + titres
         │
         ▼
2. DEDUP
   SQLite : domain existe dans processed_domains? → skip
         │
         ▼
3. ENRICHMENT (async, semaphore=10)
   A) Firecrawl /extract (schema JSON) → dirigeant, techno, email contact
   B) PageSpeed Insights API → scores SEO, perf, LCP, CWV
   Si B échoue → scoring partiel, on continue
         │
         ▼
4. SCORING ICP (/100, poids configurables dans niches.yaml)
   ≥ 60 → qualifié → étape 5
   < 60 → loggé dans data/rejected.jsonl
         │
         ▼
5. BASEROW SYNC
   POST row dans la table Leads
   Succès (200/201) confirmé → INSERT domain dans SQLite
   (= reprise automatique après crash)
```

### Reprise après crash

Le domaine est inséré en SQLite **uniquement après** confirmation du sync Baserow. Si le script crash au lead 54, au redémarrage il skip les 53 premiers (déjà en DB) et reprend au 54.

---

## 4. Structure du projet

```
lead-pipeline/
├── CLAUDE.md               # Ce fichier
├── pyproject.toml
├── docker-compose.yml      # Baserow local (dev/test)
├── config/
│   └── niches.yaml         # Niches, queries, régions, poids scoring
├── src/
│   ├── __init__.py
│   ├── cli.py              # Point d'entrée Typer (run, status)
│   ├── config.py           # Pydantic settings (env vars)
│   ├── db.py               # SQLite dedup (1 table processed_domains)
│   ├── discovery.py        # Client Firecrawl search
│   ├── enrichment.py       # Firecrawl extract + PageSpeed Insights
│   ├── scorer.py           # ICP scoring (poids configurables)
│   ├── crm.py              # Client Baserow (httpx async)
│   ├── pipeline.py         # Orchestrateur async principal
│   └── log.py              # Config structlog
├── data/
│   ├── leads.db            # SQLite (auto-créée)
│   └── rejected.jsonl      # Leads sous le seuil ICP
└── docs/
    ├── pre-claude.md        # Spec Phase 1 originale
    └── lead-pipeline-spec.md # Spec complète (référence)
```

**Règle : 1 fichier = 1 responsabilité. Pas de sous-packages inutiles.**

---

## 5. Base de données SQLite

Une seule table, uniquement pour le dedup :

```sql
CREATE TABLE IF NOT EXISTS processed_domains (
    domain TEXT PRIMARY KEY,
    processed_at TEXT NOT NULL
);
```

Pas de table leads en local. Tout va dans Baserow.

---

## 6. APIs externes

### 6.1 Firecrawl (self-hosted)

**Base URL :** `https://crawl.elevaseo.app`
**Auth :** `Authorization: Bearer {FIRECRAWL_API_KEY}`

**Discovery :**
```
POST /v1/search
{"query": "constructeur piscine Bretagne contact", "limit": 10}
→ {"data": [{"url": "...", "title": "...", "description": "..."}]}
```

**Enrichment (extract) :**
```
POST /v1/extract
{
  "urls": ["https://example.com/*"],
  "prompt": "Extract company director/CEO name, title, contact email, and website technology",
  "schema": { ... },  // JSON Schema pour structurer la sortie
  "enableWebSearch": false
}
```

### 6.2 Google PageSpeed Insights

**Endpoint :** `GET https://www.googleapis.com/pagespeedonline/v5/runPagespeed`
**Params :** `url={url}&category=performance&category=seo&strategy=mobile`
**Auth :** `key={PAGESPEED_API_KEY}` (optionnel, augmente les quotas)
**Rate limit :** 400 req/100s (gratuit)

**Données extraites :**
- `lighthouseResult.categories.performance.score` (0-1)
- `lighthouseResult.categories.seo.score` (0-1)
- `lighthouseResult.audits['largest-contentful-paint'].numericValue` (ms)
- `loadingExperience.overall_category` (FAST / AVERAGE / SLOW)

### 6.3 Baserow

**Base URL :** `http://localhost` (dev) / URL prod à définir
**Auth :** `Authorization: Token {BASEROW_TOKEN}`

**Créer un lead :**
```
POST /api/database/rows/table/{TABLE_ID}/?user_field_names=true
{
  "Entreprise": "Magiline",
  "Prenom": "Jean",
  "Nom": "Dupont",
  "Website": "https://magiline.com",
  "Domaine": "magiline.com",
  "Niche": "piscines",
  "Region": "Bretagne",
  "Score ICP": 72,
  "Score Perf": 45,
  "Score SEO": 68,
  "LCP": 4.2,
  "CWV": "SLOW",
  "Tech Stack": "WordPress",
  "Statut": "New"
}
```

**Batch create :** `POST /api/database/rows/table/{TABLE_ID}/batch/`

### 6.4 EspoCRM (Phase 2 — sync ultérieur)

**Base URL :** `https://espo.elevaseo.app`
**Auth :** `X-Api-Key: {ESPOCRM_API_KEY}`
**Usage :** Sync Baserow → EspoCRM quand pipeline outreach prêt.

---

## 7. Scoring ICP

Score sur 100. Seuil de qualification : **≥ 60**.

### Critères et poids (configurables dans `niches.yaml`)

| Critère | Points | Condition |
|---|---|---|
| Score Performance bas | 25 | PageSpeed performance < 0.50 |
| Score SEO bas | 25 | PageSpeed SEO < 0.80 |
| LCP lent | 15 | LCP > 4000ms |
| Core Web Vitals fail | 15 | CWV overall = SLOW |
| Pas de HTTPS | 10 | URL en http:// |
| WordPress détecté | 10 | Tech stack = WordPress |

### Config YAML

```yaml
scoring:
  threshold: 60
  weights:
    perf_under_50: 25
    seo_under_80: 25
    lcp_over_4s: 15
    cwv_fail: 15
    no_https: 10
    wordpress: 10
```

Les poids sont des variables modifiables sans toucher au code.

---

## 8. Configuration — `config/niches.yaml`

```yaml
scoring:
  threshold: 60
  weights:
    perf_under_50: 25
    seo_under_80: 25
    lcp_over_4s: 15
    cwv_fail: 15
    no_https: 10
    wordpress: 10

niches:
  piscines:
    queries:
      - "constructeur piscine {region} site officiel"
      - "pisciniste {region} contact"
      - "fabricant piscine coque polyester {region}"
  salles_de_sport:
    queries:
      - "salle de sport fitness {region} site web"
      - "club fitness {region}"
  # ... Aucune limitation sur le nombre de niches

regions:
  - Île-de-France
  - Auvergne-Rhône-Alpes
  - Provence-Alpes-Côte d'Azur
  - Nouvelle-Aquitaine
  - Occitanie
  - Bretagne
  - Hauts-de-France
  - Grand Est
  - Pays de la Loire
  - Normandie
  - Centre-Val de Loire
  - Bourgogne-Franche-Comté
  - Corse
```

---

## 9. Interface CLI

```bash
# Pipeline complet
uv run lead-pipeline run --niche piscines --region "Bretagne" --limit 100

# Mode test (debug rapide)
uv run lead-pipeline run --niche piscines --region "Bretagne" --limit 3 --dry-run

# Statut des domaines traités
uv run lead-pipeline status

# Toutes niches, toutes régions
uv run lead-pipeline run-all --limit-per-niche 20
```

### Options CLI

| Flag | Défaut | Description |
|---|---|---|
| `--niche` | requis | Niche cible |
| `--region` | requis | Région cible |
| `--limit` | 100 | Nombre max de leads par run |
| `--dry-run` | false | Affiche les leads sans pousser dans Baserow |
| `--concurrency` | 10 | Nombre de requêtes async parallèles |

---

## 10. Logging (structlog)

- **Console :** Format rich lisible (dev)
- **Fichier :** JSON lines dans `data/logs/pipeline-{date}.jsonl`

```python
log.info("discovery.search", niche="piscines", region="Bretagne", results=15)
log.info("dedup.skip", domain="magiline.com", reason="already_processed")
log.info("enrichment.done", domain="example.com", director="Jean Dupont", tech="WordPress")
log.warning("enrichment.pagespeed_failed", domain="example.com", error="timeout")
log.info("scoring.qualified", domain="example.com", score=72)
log.info("scoring.rejected", domain="bad-site.com", score=35)
log.info("crm.synced", domain="example.com", baserow_row_id=42)
log.error("crm.sync_failed", domain="example.com", status=500, retry=2)
```

---

## 11. Gestion des erreurs

- **Retry :** `tenacity` avec backoff exponentiel (3 tentatives, wait 2-10s)
- **Enrichment partiel :** Si PageSpeed échoue, on score avec les données Firecrawl seules (score partiel)
- **Crash recovery :** Dedup SQLite = point de reprise automatique
- **Rate limits :** `asyncio.Semaphore(10)` pour la concurrence, `tenacity` pour les 429

---

## 12. Sécurité

- **Jamais lire/écrire les fichiers `.env`** — utiliser `pydantic-settings` pour charger les env vars
- **Variables d'environnement requises :**
  - `FIRECRAWL_API_KEY`
  - `BASEROW_TOKEN`
  - `BASEROW_TABLE_ID`
  - `PAGESPEED_API_KEY` (optionnel)

---

## 13. Docker — Baserow local (dev/test)

```yaml
# docker-compose.yml
services:
  baserow:
    image: baserow/baserow:2.1.0
    environment:
      BASEROW_PUBLIC_URL: 'http://localhost'
    ports:
      - "80:80"
    volumes:
      - baserow_data:/baserow/data

volumes:
  baserow_data:
```

```bash
docker compose up -d    # Lancer
# → http://localhost → Créer compte → Créer table "Leads"
# → Settings → Database tokens → Créer token
```

### Champs Baserow à créer

| Champ | Type | Description |
|---|---|---|
| Entreprise | Text | Nom de l'entreprise |
| Prenom | Text | Prénom du dirigeant |
| Nom | Text | Nom du dirigeant |
| Website | URL | Site web complet |
| Domaine | Text | Domaine extrait |
| Email Contact | Email | Email trouvé sur le site |
| Niche | Single Select | Niche du lead |
| Region | Single Select | Région |
| Score ICP | Number | Score /100 |
| Score Perf | Number | PageSpeed performance (0-100) |
| Score SEO | Number | PageSpeed SEO (0-100) |
| LCP | Number | Largest Contentful Paint (secondes) |
| CWV | Single Select | FAST / AVERAGE / SLOW |
| Tech Stack | Text | Technologies détectées |
| Statut | Single Select | New / Researching / Contacted |

---

## 14. Roadmap

### Phase 1 (actuelle)
- [x] Discovery (Firecrawl search)
- [x] Dedup SQLite
- [x] Enrichment (Firecrawl extract + PageSpeed)
- [x] Scoring ICP configurable
- [x] Baserow sync
- [x] CLI (run, status, dry-run)

### Phase 2 (future)
- [ ] Email Finder (permutations + SMTP verify)
- [ ] Sync Baserow → EspoCRM
- [ ] API INSEE/SIRENE fallback (dirigeants FR)
- [ ] Outreach (séquence cold email)
- [ ] Mode daemon (scheduler)
