CompanyData — Roadmap
État du produit : ce qui tourne en production et ce qui reste à faire. Service auto-hébergé d'enrichissement d'entreprise, validation d'e-mail, normalisation d'adresse et nettoyage de référentiel — sans dépendance à un fournisseur SaaS tiers.
- Prod :
https://companydata.scale2sell.company(VPS vulcain, Docker Compose) - Contrat d'API :
/api-doc· Swagger :/docs - Dernière mise à jour : 2026-07-28
Légende : ✅ en production · 🟡 partiel / à finir · 🔭 à faire · 🧱 dette technique.
✅ En production
Enrichissement d'entreprise
- ✅ Enrichissement par domaine, URL ou e-mail (
/enrich,/enrich/batch). - ✅ Pipeline de scraping (homepage + pages légales/contact) + extraction heuristique.
- ✅ Repli LLM opt-in (
?llm=true) pour l'extraction difficile ; provider-agnostique. - ✅ LLM knowledge : si le site est bloqué/injoignable, identification via la mémoire du modèle.
- ✅ Méta-domaines : parapluies pro (notaires.fr…), builders (tm.fr…), hébergeurs — anti-alias + portails.
- ✅ Repli dichotomique + alias : un sous-domaine stérile remonte au parent, les deux fiches stockées.
- ✅ Registre FR (Recherche d'entreprises DINUM) : SIREN, raison sociale, adresse siège, NAF, finances.
- ✅ Filtre hébergeur : ne jamais prendre l'hébergeur (OVH, Gandi…) pour la société.
- ✅ Champs structurés : ville / CP / pays, baseline, contacts (nom + rôle), réseaux sociaux, tech stack.
- ✅ Fusion non destructive + péremption adaptative (9 mois, courte si données pauvres / domaine en vente).
Enrichissement par SIREN/SIRET
- ✅
GET /enrich/siren: entrée = SIREN (9) ou SIRET (14, tronqué au siège). Renvoie l'identité légale (registre DINUM : raison sociale, adresse siège, NAF, effectif, création, dirigeants, finances, TVA, état actif/radié). - ✅ Pont SIREN → domaine en cascade (on garde le 1er fiable) : référentiel (fiche déjà
enrichie) →
domain_hintvalidé (site_siren == siren, garde SSRF) → recherche « grounded » (métamoteur SearXNG auto-hébergé + le LLM identifie le domaine officiel parmi de VRAIS résultats, anti-hallucination ; ex. OVH → ovhcloud.com) → repli slugs (validés par le pipeline) → sinondomain:null(holding sans site = réponse nominale). Confiance : SIREN prouvé par le site →high, choix LLM grounded →medium. Renvoie une liste classéedomains: [{domain, source, confidence}](plusieurs domaines possibles — ex. marque.fr + marque.com) ;domain= tête de liste (compat). Le modewebfusionne référentiel + hint + recherche (ne court-circuite plus) : on récupère aussi les domaines liés qu'une seule source connaît (ex. holding → scale2sell.company au référentiel et we-upgrade.fr par la recherche). Le modereferential(défaut) reste un court-circuit rapide. - ✅ Logo dès qu'un domaine est rattaché ; fiche complète si
full=true(écrit le référentiel). Read-only sinon (pas de fiche fantôme). Cachesiren_domains(résolutions web, TTL 30 j). Webhookcompany.resolved_by_siren. - ✅ Réinjection (
reinject=true, mode web) : les domaines découverts absents du référentiel sont enrichis en fiches (les marques manquantes du groupe entrent dans la base) ; une même-SIREN détectée ensuite par Clean Review peut être arbitrée (fusion vs marques distinctes).
Adresses & BAN
- ✅ Normalisation d'adresse via Base Adresse Nationale (
/normalise/address*). - ✅ Déduplication d'adresses (surrogate
addr:<hash>), statut resolved / ambiguous. - ✅ Comparaison de voies robuste (types, abréviations, St→Saint, numéro de voirie discriminant).
- ✅ Arbitrage d'adresse par LLM (voir ci-dessous).
Validation d'e-mail
- ✅ Validation syntaxe + MX + heuristiques (
/validate/email,/validate/batch). - ✅ Sonde SMTP optionnelle, jamais d'envoi réel.
- ✅ Verdicts explicités (raison + signaux), score.
- ✅ Bounces et deliverables (feedback réel) :
/bounces*,/deliverables*→ ajuste le statut.
Validation de téléphone (international)
- ✅ Couche gratuite hors-ligne via
libphonenumber(/validate/phone,/validate/phone/batch) : validité, E.164 / national / international, type de ligne (mobile/fixe/VoIP/gratuit/surtaxé/ pager…), pays, indicatif, localisation, fuseau(x) horaire(s), opérateur d'origine. Déterministe, tous pays, sans coût ni réseau. - ✅ Couche HLR/SS7 opt-in (
?hlr=true) : opérateur actuel (portage) + état live de la ligne via un dip IPQualityScore. Dormante tant que la clé n'est pas configurée ; résultats mis en cache (tablephone_validations, péremption 90 j) pour ne jamais re-payer un numéro. Best-effort : jamais bloquant, aucun appel payant sur un numéro invalide. - ✅ Portage fiable via MCC/MNC : l'opérateur servant (réseau réel) est dérivé du MCC/MNC
(table mondiale embarquée, 238 pays), pas du champ
carrierd'IPQS (= opérateur d'origine, faux sur un numéro porté — Abstract fait la même erreur). Dérivé à la lecture → les lignes en cache se corrigent sans re-payer.
Email Finder (devine l'e-mail d'un contact)
- ✅ Génération de candidats à partir du nom + domaine (10 motifs courants :
{first}.{last},{f}{last},{first}…), motif préféré en tête. - ✅ Inférence du motif d'un domaine depuis les couples e-mail↔contact déjà connus, mis en cache
sur
companies.email_pattern(une entreprise « devine » mieux au fil des données). - ✅ Vérification SMTP opt-in (RCPT, jamais d'envoi) avec arrêt anticipé sur délivrable, gestion catch-all ; confiance graduée verified > pattern > guess.
- ✅ Couche feedback (bounce/deliverable) :
find_emailconsulte l'historique des envois réels et consentis AVANT toute sonde. Un candidat déjà délivrable confirmé (engagement open/clic) estverifiedsans sonde — seul moyen fiable pour Gmail / Google Workspace (MX qui ne répond pas honnêtement au RCPT) ; un candidat déjà bouncé est écarté. On ne fabrique jamais d'envoi pour vérifier. - ✅ Gmail vs Workspace :
provider_kinddistingue la boîte grand public@gmail.comdu domaine d'entreprise sur MX Google ; message explicite « non sondable, à confirmer par un envoi réel ». - ✅ Endpoints
/find/email,/find/email/batch(max 200),/companies/{domain}/email-pattern. - ✅ Intégré à l'import : une ligne sans e-mail mais avec prénom + nom + domaine est complétée
automatiquement (un résultat vérifié remplit l'e-mail, sinon il part en validation). Colonnes
export
email_trouvé/email_confiance.
Civilité & Identité
- ✅ Référentiel INSEE prénoms (11 583 noms) → sexe.
- ✅ Découpe nom, salutation, salutation intelligente (titre/fonction), config par workspace.
- ✅ Routes
/v1/civility*,/v1/workspaces/{id}/salutation-config.
Import CSV
- ✅ Scan rapide (dédup e-mails/domaines) et Import enrichi ligne à ligne (mapping 2 axes).
- ✅ Chaque ligne ressort avec délivrabilité, entreprise, région/département, civilité, téléphone E.164.
- ✅ Export CSV (fichier d'origine + colonnes enrichies).
- ✅ Téléchargement partiel même si l'import n'est pas terminé (failed/interrompu).
- ✅ Vue d'import dernier enrichi en premier (suivi en direct).
- ✅ Reprise des imports orphelins au redémarrage (marqués
failed, relançables).
Clean Review (nettoyage du référentiel)
- ✅ Moteur de règles (RULESET v17) : incohérences nom / SIREN / description / adresse / ville / pays / réseaux.
- ✅ SIREN partagé :
duplicate_company(identités compatibles → doublon à fusionner) vssiren_misattributed(identités divergentes → homonyme / attribution erronée, vider le SIREN faux). - ✅ File de revue unifiée (entreprises + adresses), actions inline dans la fiche.
- ✅ Fermeture des alertes sœurs (un topic tranché clôt les alertes liées).
- ✅ File d'attente : on enchaîne les fiches, bouton « Passer », indicateur de position.
Arbitrage IA — moteur Clean Review (Sonnet)
- ✅ SIREN : le LLM juge si le SIREN d'une fiche lui appartient en comparant son identité au
nom légal du SIREN au registre DINUM (gratuit) — distingue « marque ≠ raison sociale » (normal)
d'une attribution erronée (homonyme / scraping ; ex. lafermiere.com portant le SIREN d'OVH).
clearà confiance haute → vide le SIREN (réversible) et clôt l'alerte ; sinon reco ★ IA. EndpointsPOST /companies/{domain}/siren/arbitrateetPOST /review/siren/arbitrate(lot, background). - ✅ Doublons : le LLM tranche même entité (→ reco fusion, domaine canonique proposé) vs
homonymes distincts (→ classe l'alerte faux positif si confiance haute, réversible ; la fusion
reste une action humaine).
POST /review/duplicate/arbitrate. - ✅ Dispatcher unifié
POST /review/arbitrate: arbitre en un appel adresse (entreprise + normalisation), SIREN et doublons —background=truepour vider tout le stock. - ✅ Adresse — par entreprise : le LLM tranche site / registre / BAN à partir du texte du site (politique : adresse d'exploitation prioritaire). Auto-applique si confiance haute, sinon reco pré-remplie.
- ✅ Normalisation (brut ↔ BAN) : le LLM juge la proposition BAN → ban / correction / keep.
- ✅ Endpoints
/companies/{domain}/address/arbitrate,/review/address/arbitrate,/review/address/normalize-arbitrate(synchrone ou tâche de fond). - ✅ Traçabilité (
review_suggestions.llm: choix, confiance, rationale, modèle). - ✅ Rattrapage du stock initial : ~480 adresses auto-résolues, ~80 pré-remplies, 0 échec.
Plateforme
- ✅ IHM autonome (une page, vanilla JS) : référentiel, e-mails, logos, hébergeurs, agences, Clean Review, imports, adresses, webhooks.
- ✅ Auth
X-API-Key. Webhooks signés (HMAC) surcompany.updated/address.updated. - ✅ Cache d'images (logo/avatar) avec péremption. Notifications Slack par enrichissement.
- ✅ Contrat d'API rendu en HTML (
/api-doc), Swagger, OpenAPI JSON. - ✅ 434 tests (pytest, sans réseau). Migrations Alembic (0001 → 0039).
🔭 À faire — backlog produit
- 🔭 Auto-arbitrage après chaque scan : arbitrer les nouvelles alertes d'adresse sans action manuelle.
- 🔭 Prompt caching sur les prompts système d'arbitrage (‑~90 % du coût entrée sur gros volumes).
- 🔭 Décompte temps réel des fiches restantes dans la file de revue (au lieu de la position figée).
- 🔭 Choix : verrouillage vs ré-enrichissement corrigeable pour les décisions LLM auto-appliquées.
- 🔭 Modèle d'arbitrage : passer à Sonnet 5 quand validé (variable
LLM_ARBITER_MODEL). - 🔭 Vue d'ensemble Clean Review : tableau de bord par type d'alerte + tendances.
- 🔭 Export enrichi : formats additionnels (xlsx), colonnes configurables.
🧱 Dette technique & architecture
Base de données
- ✅ Index de performance (migration 0035) :
import_rows(import_id,row_index)et(import_id,processed_at),review_suggestions(status,created_at)et(rule),companies(status),enrichment_runs(company_id,run_at),email_validations(score). Purement additif. - 🧱 Adresse dénormalisée :
companiesporte une copie à plat de l'adresse et la tableaddressesexiste, sans clé étrangère entre les deux (lien souple paraddress_ban_id). Intégrité non garantie. - 🧱 Colonne polymorphe :
review_suggestions.domaincontient soit un domaine, soitaddr:<hash>(discriminé parsubject_type) — file unifiée pratique mais empêche les jointures. - 🧱 JSON à normaliser :
companies.contacts([{name,role}]) etcompanies.tags(filtré par cast JSON→texte, non indexable) mériteraient des tables filles. - 🧱 Défauts serveur manquants : plusieurs colonnes JSON nullable (
emails, phones, contacts, tech_stack, overrides…) sansserver_default→ d'anciennes lignes peuvent êtreNULLau lieu de[]/{}.addresses.overrides(0028) le fait bien : à harmoniser. - 🧱 Nommage :
postal_code(companies, import_rows) vspostcode(addresses). - 🔭 Clés étrangères réelles (
companies→addresses,hosting_domain→hosts) ; recherche trigram surcompanies.domainet les colonnes d'adresse si ces recherches deviennent chaudes.
Versioning & sécurité
- ✅ Dépôt Git initialisé (branche
main,.gitignorecouvrant.env/.venv/artefacts). C'était la priorité n°1 — un filet de sécurité qui rend tout refactor réversible. - 🔭 Remote + CI : pousser vers un remote (GitHub/GitLab) + CI (pytest à chaque push).
- 🔭 Déploiement versionné : remplacer le
tar | sshpar un déploiement lié à un tag/commit.
Code & architecture
- ✅ Nettoyage (fait cette nuit) : code mort supprimé (
RegistryData,_name_from_meta, imports inutilisés) ; endpoints hôtes/agences factorisés (_list_entities/_get_entity_detail) ; pages documentaires factorisées (_render_markdown_pagepour/api-doc+/roadmap). - ✅ Monolithe
main.pydécoupé (fait) : 3723 → 221 lignes (−94 %). 12 routers FastAPI par domaine (pages, webhooks, civility, images, validation, addresses, review, companies, entities, imports, stats, finder) + modules de service partagés (deps, util, image_service, validation_service, address_service, companies_service, imports_service, email_finder).main.pyne contient plus que le montage de l'app, les hooks de démarrage et le ré-essai finances. 393 tests verts, comportement identique. - 🧱 Fonctions longues à décomposer :
pipeline.run_enrichment(~330 l., étapes déjà balisées),imports_service._process_import_row(drivers vs normalizers),review.run_scan,review.accept_suggestion. - 🔭 Dédup restante :
_get_or_404(28 sites),_lock_fields(idiome overrides ×9),_csv_response(2 exports +ban.py), cœur communnormalise_addressGET/POST,_enforce_batch_limit(6 gardes, unifier « par lot »). - 🔭 Cohérence :
response_modelsur les routes de mutation (29/67 typées) ;datetime.now→ helperutcnow()(33 sites) ;@app.on_event→lifespan(FastAPI moderne).
Roadmap générée depuis docs/ROADMAP.md et servie sur /roadmap.