france-data-mcp 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/coords.ts","../../src/core/http.ts","../../src/core/lookup-result.ts","../../src/core/numbers.ts","../../src/core/object-utils.ts","../../src/sante/insee-sirene.ts","../../src/sante/dinum.ts","../../src/core/cache.ts","../../src/core/csv.ts","../../src/sante/finess-categories.ts","../../src/sante/finess.ts","../../src/sante/annuaire-ameli.ts","../../src/sante/naf-codes.ts","../../src/core/query-metadata.ts","../../src/storage/supabase.ts","../../src/sante/db-helpers.ts","../../src/sante/finess-db.ts","../../src/territoire/dept-codes.ts","../../src/sante/rpps-types.ts","../../src/sante/rpps-db.ts","../../src/sante/ans-fhir.ts","../../src/sante/index.ts"],"names":["FETCH_TIMEOUT_MS"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Helpers internes pour la conversion `lon/lat string|number → Coordinates`.\n * Pas exporté publiquement (usage strictement interne aux mappers d'API).\n *\n * Pourquoi un helper ? Les CSV FINESS utilisent la virgule décimale FR\n * (\"4,7192\") tandis que l'API DINUM renvoie soit `string` ASCII (\"4.7192\")\n * soit `number`. La même logique de validation `Number.isFinite` est ensuite\n * appliquée des deux côtés, d'où la mutualisation.\n */\nimport type { Coordinates } from \"./types.js\";\n\n/**\n * Convertit une paire `lon/lat` (string ou number, virgule ou point décimal)\n * en `Coordinates` validées. Renvoie `undefined` si l'un des deux est absent\n * (`undefined` OU `null` — l'API DINUM utilise `null` pour les champs vides)\n * ou non finite — préférable à des coordonnées (NaN, NaN) qui pollueraient\n * silencieusement la suite du pipeline.\n */\nexport function parseCoordinates(\n lon: string | number | null | undefined,\n lat: string | number | null | undefined,\n): Coordinates | undefined {\n const lonNum = parseLooseNumber(lon);\n const latNum = parseLooseNumber(lat);\n if (lonNum === undefined || latNum === undefined) return undefined;\n return { lon: lonNum, lat: latNum };\n}\n\nfunction parseLooseNumber(value: string | number | null | undefined): number | undefined {\n // `value == null` couvre `undefined` ET `null`. L'API DINUM renvoie `null`\n // (pas `undefined`) quand `longitude`/`latitude` sont vides côté SIRENE ;\n // sans ce garde, `value.replace(...)` plus bas crash avec\n // `Cannot read properties of null`.\n if (value == null) return undefined;\n if (typeof value === \"number\") return Number.isFinite(value) ? value : undefined;\n // Les CSV FR (FINESS, INSEE…) utilisent la virgule comme séparateur décimal.\n const normalized = value.replace(\",\", \".\");\n const num = Number.parseFloat(normalized);\n return Number.isFinite(num) ? num : undefined;\n}\n","/**\n * HTTP helper avec retry exponentiel et respect du header `retry-after`.\n *\n * Conçu pour les API publiques françaises qui :\n * - retournent HTTP 429 avec un header `retry-after` en secondes,\n * - peuvent bannir une IP en cas de spam (DINUM API Entreprise → bannissement 12h non-révocable),\n * - apprécient un User-Agent identifiable pour pouvoir contacter en cas d'usage anormal.\n */\n\nimport type { RateLimitOptions } from \"./types.js\";\n\nexport const DEFAULT_USER_AGENT =\n \"france-data-mcp/0.1.0 (+https://github.com/cturkieh/france-data-mcp)\";\n\nexport class HttpError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly url: string,\n public readonly body?: string,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\nexport class RateLimitExceededError extends HttpError {\n constructor(url: string, retryAfter: number) {\n super(`Rate limit exceeded after retries on ${url} (retry-after: ${retryAfter}s)`, 429, url);\n this.name = \"RateLimitExceededError\";\n }\n}\n\ntype FetchJsonOptions = RateLimitOptions & {\n headers?: Record<string, string>;\n signal?: AbortSignal;\n};\n\n/**\n * GET une URL et parse la réponse JSON, avec retry exponentiel sur 429 et 5xx.\n *\n * - Sur 429 : respecte `retry-after` (secondes) si présent, sinon backoff exponentiel.\n * - Sur 5xx : backoff exponentiel.\n * - Sur 4xx (sauf 429) : throw immédiatement (erreur logique, pas un retry).\n * - User-Agent par défaut identifie la lib et le repo (pour traçabilité).\n */\nexport async function fetchJson<T>(url: string, options: FetchJsonOptions = {}): Promise<T> {\n const {\n maxRetries = 3,\n baseDelayMs = 500,\n userAgent = DEFAULT_USER_AGENT,\n headers = {},\n signal,\n } = options;\n\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(url, {\n headers: {\n Accept: \"application/json\",\n \"User-Agent\": userAgent,\n ...headers,\n },\n signal,\n });\n\n if (response.ok) {\n return (await response.json()) as T;\n }\n\n if (response.status === 429) {\n const retryAfter = parseRetryAfter(response.headers.get(\"retry-after\"));\n if (attempt === maxRetries) {\n throw new RateLimitExceededError(url, retryAfter);\n }\n await sleep(retryAfter * 1000 + jitter());\n continue;\n }\n\n if (response.status >= 500 && response.status < 600 && attempt < maxRetries) {\n await sleep(baseDelayMs * 2 ** attempt + jitter());\n continue;\n }\n\n const body = await response.text().catch((bodyErr: unknown) => {\n console.warn(\n `[france-data-mcp] failed to read response body for ${url}: ${(bodyErr as Error).message}`,\n );\n return undefined;\n });\n throw new HttpError(\n `HTTP ${response.status} on ${url}`,\n response.status,\n url,\n body?.slice(0, 500),\n );\n } catch (err) {\n if (err instanceof HttpError) throw err;\n lastError = err as Error;\n // Une SyntaxError du JSON parser veut dire que l'API a renvoyé un body non-JSON\n // (HTML d'erreur, page de maintenance…). Le retry ne servira à rien — on échoue vite.\n if (lastError instanceof SyntaxError) {\n console.error(`[france-data-mcp] invalid JSON response from ${url}: ${lastError.message}`);\n throw lastError;\n }\n // Si le caller a annulé via AbortSignal, ne pas tenter de retry — un\n // signal déjà aborté ne peut plus être ré-utilisé. Sans ce shortcircuit,\n // les 3 retries restants se feraient contre `signal.aborted=true` avec\n // attentes setTimeout cumulées qui dépasseraient le timeout caller.\n // Log différencié : \"vraie\" AbortError (signal.aborted ET err name match)\n // vs erreur réseau survenue juste avant l'abort (race) — sans ça, un\n // ENOTFOUND in-flight pourrait être silencé sous le label \"abort\".\n if (lastError.name === \"AbortError\") {\n console.warn(`[france-data-mcp] fetch aborted (caller signal) on ${url}`);\n throw lastError;\n }\n if (signal?.aborted) {\n console.warn(\n `[france-data-mcp] fetch aborted on ${url} (signal already aborted) — last error: ${lastError.message}`,\n );\n throw lastError;\n }\n const isFinalAttempt = attempt === maxRetries;\n const log = isFinalAttempt ? console.error : console.warn;\n log(\n `[france-data-mcp] network error on ${url} (attempt ${attempt + 1}/${maxRetries + 1}): ${lastError.message}`,\n );\n if (isFinalAttempt) break;\n await sleep(baseDelayMs * 2 ** attempt + jitter());\n }\n }\n\n console.error(`[france-data-mcp] giving up on ${url} after ${maxRetries + 1} attempts`);\n throw lastError ?? new Error(`Unknown failure fetching ${url}`);\n}\n\nfunction parseRetryAfter(header: string | null): number {\n if (!header) return 5;\n // Cap à 60 s : si une API exige une attente plus longue, on préfère échouer\n // (et laisser le caller gérer) plutôt que bloquer un handler MCP/serveur.\n const seconds = Number.parseInt(header, 10);\n if (Number.isFinite(seconds) && seconds > 0) return Math.min(seconds, 60);\n // Format HTTP-date (RFC 7231 §7.1.3) : \"Wed, 21 Oct 2015 07:28:00 GMT\"\n const dateMs = Date.parse(header);\n if (Number.isFinite(dateMs)) {\n const deltaSec = Math.ceil((dateMs - Date.now()) / 1000);\n if (deltaSec > 0) return Math.min(deltaSec, 60);\n }\n return 5;\n}\n\nfunction jitter(): number {\n return Math.floor(Math.random() * 250);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * Type partagé pour les lookups d'entité unique (par identifiant).\n *\n * Pourquoi : retourner `null` brut quand un identifiant n'est pas trouvé est\n * un silent failure — le caller MCP ne peut pas distinguer \"introuvable\" de\n * \"panne API\" ni obtenir un message actionnable. Pattern aligné sur\n * `enrichmentStatus` (cf. `src/sante/dinum.ts`) qui a déjà fait ses preuves.\n *\n * Le discriminant `found: boolean` permet au caller de narrower côté TS et\n * facilite la lecture côté LLM : un agent voit immédiatement le statut au\n * lieu de devoir tester `=== null`.\n *\n * Cas d'usage actuels (v0.4.3) :\n * - `getEntrepriseBySiren` (DINUM)\n * - `getCommuneByCode` (geo.api.gouv)\n * - `getFinessByNumFiness` (FINESS / DREES)\n *\n * Pour les listes (radius, dept, etc.), `count: 0` + `results: []` reste le\n * pattern adapté — pas besoin de ce type.\n */\n\n/**\n * Statuts possibles pour un lookup. Les valeurs reflètent les causes\n * sémantiquement distinctes pour le caller :\n *\n * - `found` : entité trouvée et retournée.\n * - `not_found` : identifiant absent du référentiel cible (cas le plus courant).\n * - `ambiguous` : l'API a renvoyé des résultats mais aucun ne matche\n * exactement l'identifiant fourni — typiquement un signal de régression\n * amont (recherche full-text qui matche sur autre chose), à surveiller.\n */\nexport type LookupStatus = \"found\" | \"not_found\" | \"ambiguous\";\n\n/**\n * Cas \"introuvable\" d'un lookup. `key` reflète l'identifiant fourni par le\n * caller (siren, code INSEE, num_finess…) pour faciliter le debug côté agent.\n * `message` doit être actionnable — orienter vers une alternative quand elle\n * existe (ex: `entreprises_in_radius` pour SIREN en diffusion partielle).\n */\nexport interface LookupNotFound {\n found: false;\n /** Identifiant fourni par le caller (siren / code INSEE / num_finess / …). */\n key: string;\n lookupStatus: Exclude<LookupStatus, \"found\">;\n message: string;\n}\n\n/**\n * Forme générique d'un résultat de lookup. `T` doit être l'entité brute\n * (sans champ discriminant) — le wrapper ajoute `found: true` et\n * `lookupStatus: \"found\"` au moment du return.\n */\nexport type LookupResult<T> = (T & { found: true; lookupStatus: \"found\" }) | LookupNotFound;\n\n/** Helper pour wrapper un résultat trouvé sans répétition au call-site. */\nexport function lookupFound<T>(entity: T): T & { found: true; lookupStatus: \"found\" } {\n return { ...entity, found: true, lookupStatus: \"found\" };\n}\n\n/** Helper pour produire un résultat \"introuvable\" typé. */\nexport function lookupNotFound(\n key: string,\n message: string,\n status: Exclude<LookupStatus, \"found\"> = \"not_found\",\n): LookupNotFound {\n return { found: false, key, lookupStatus: status, message };\n}\n","/**\n * Helpers numériques internes (pas exportés publiquement).\n */\n\n/**\n * Borne `value` dans l'intervalle `[min, max]`. Aucune coercition : si la\n * valeur est NaN, elle reste NaN — le caller doit la valider en amont.\n */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\n/**\n * Convertit des mètres en kilomètres arrondis à 2 décimales (10 m de précision).\n * Cohérent entre les wrappers FINESS et Ameli — `distance_meters` (PostGIS\n * `ST_Distance` géography) → `distance_km` exposé au caller MCP.\n *\n * Renvoie `null` si l'entrée est `null`/`undefined` ou non-numérique : utilisé\n * sur les RPCs *_by_dept où `distance_meters` est `NULL::DOUBLE PRECISION` car\n * il n'y a pas de centre de référence.\n */\nexport function metersToKm(meters: number | null | undefined): number | null {\n if (typeof meters !== \"number\" || !Number.isFinite(meters)) return null;\n return Math.round((meters / 1000) * 100) / 100;\n}\n","/**\n * Helpers internes pour la construction d'objets de retour à partir de payloads\n * d'API (DINUM, FINESS, Ameli, geo.api.gouv.fr, IGN). Pas exporté publiquement.\n *\n * Pourquoi un helper ? Les mappers `toEtablissement`, `toProfessionnelSante`,\n * etc. répétaient ~45 fois le pattern `if (api.X) e.Y = api.X` pour omettre\n * les champs absents/vides du payload. La version déclarative (mapping colonne\n * → champ métier) est plus lisible et plus facile à amender quand l'API\n * upstream renomme une colonne.\n */\n\n/**\n * Filtre les entrées dont la valeur est `undefined` ou la chaîne vide.\n * Préserve les autres valeurs telles quelles. Renvoie un `Partial<T>` qui\n * peut être spread dans un littéral d'objet.\n *\n * @example\n * ```ts\n * const ps: ProfessionnelSante = {\n * nom, prenom,\n * ...pickDefined({\n * civilite: row.ps_activite_civilite,\n * codePostal: row.coordonnees_code_postal,\n * }),\n * };\n * ```\n */\nexport function pickDefined<T extends Record<string, string | undefined>>(obj: T): Partial<T> {\n const out: Partial<T> = {};\n for (const key in obj) {\n const value = obj[key];\n if (value !== undefined && value !== \"\") {\n out[key] = value;\n }\n }\n return out;\n}\n","/**\n * INSEE SIRENE V3.11 — fallback authentifié pour les SIREN absents de DINUM.\n *\n * Pourquoi : `recherche-entreprises.api.gouv.fr` (DINUM) exclut les entreprises\n * en diffusion partielle INSEE (`statut_diffusion ∈ {P,N}`). Cas connu : SIREN\n * 787120435 (BIO ARD'AISNE) présent dans SIRENE mais absent de DINUM. Pour ces\n * SIREN, on fallback sur l'API SIRENE INSEE directement (clé requise).\n *\n * Auth (vérifié 2026-05-09 sur portail-api.insee.fr V3.11) :\n * - Header **`X-INSEE-Api-Key-Integration: <api-key>`** (UUID issu du portail).\n * Bearer / apikey / X-Gravitee-Api-Key tous renvoient 401 — c'est le custom\n * header Gravitee configuré côté gateway INSEE qui prime.\n * - Endpoint : `GET https://api.insee.fr/api-sirene/3.11/siren/{siren}`\n * - Rate limit : 30 req/min (header `x-rate-limit-limit: 30`).\n *\n * Payload V3.11 : les champs métier (denomination, nom, prénom, NAF, état\n * administratif, catégorie juridique) sont dans\n * `uniteLegale.periodesUniteLegale[0]` (la période la plus récente, ordre\n * antéchronologique), PAS sur `uniteLegale` directement.\n *\n * No-op gracieux : si `INSEE_SIRENE_API_KEY` n'est pas configurée,\n * `lookupSirenViaInsee` retourne null sans throw — la lib reste utilisable\n * sans clé INSEE.\n */\n\nimport { HttpError, fetchJson } from \"../core/http.js\";\nimport { type LookupResult, lookupFound, lookupNotFound } from \"../core/lookup-result.js\";\nimport type { Entreprise } from \"./dinum.js\";\n\nconst SIRENE_BASE_URL = \"https://api.insee.fr/api-sirene/3.11\";\n/** Nom exact du header attendu par l'API gateway Gravitee côté INSEE V3.11. */\nconst INSEE_AUTH_HEADER = \"X-INSEE-Api-Key-Integration\";\n/**\n * Timeout côté caller. Doit couvrir TOUS les retries `fetchJson` (jusqu'à 4\n * tentatives avec backoff exponentiel ~0.5+1+2+4s = 7.5s + temps de requête).\n * 60s laisse une marge confortable même sous lenteur INSEE 5xx.\n */\nconst FETCH_TIMEOUT_MS = 60_000;\n\n/**\n * Lit `INSEE_SIRENE_API_KEY` depuis l'env. Retourne `null` si absente ou vide\n * (no-op gracieux : la lib reste utilisable sans clé INSEE).\n *\n * Strippe aussi les guillemets entourants — certains parsers `.env` (ou un\n * copier-coller Vercel UI) les conservent, et l'API INSEE rejette alors\n * silencieusement la clé en 401, ce qui ressemble à une clé révoquée.\n */\nexport function getInseeApiKey(): string | null {\n const raw = process.env.INSEE_SIRENE_API_KEY;\n if (!raw) return null;\n const cleaned = raw.trim().replace(/^[\"']|[\"']$/g, \"\");\n return cleaned === \"\" ? null : cleaned;\n}\n\n/**\n * Shape minimale de la réponse INSEE SIRENE V3 — on lit uniquement les champs\n * nécessaires au mapping `Entreprise`. Les valeurs métier vivent dans\n * `periodesUniteLegale[0]` (la période courante).\n *\n * Exporté pour permettre aux tests de typer leurs fixtures (`Partial<ApiInseePeriode>`)\n * et bénéficier de l'autocomplete + détection de typos sur les noms de champs.\n */\nexport type ApiInseePeriode = {\n dateFin?: string | null;\n dateDebut?: string | null;\n denominationUniteLegale?: string | null;\n nomUniteLegale?: string | null;\n prenomUsuelUniteLegale?: string | null;\n prenom1UniteLegale?: string | null;\n activitePrincipaleUniteLegale?: string | null;\n etatAdministratifUniteLegale?: string | null;\n categorieJuridiqueUniteLegale?: string | null;\n};\n\n/**\n * Shape `uniteLegale` SIRENE V3.11. Volontairement permissive sur les deux\n * formats que l'API expose :\n *\n * - Endpoint **`/siren/{siren}`** → `uniteLegale.periodesUniteLegale[]` (historisé\n * chronologiquement, période courante = `dateFin: null`).\n * - Endpoint **`/siret/{siret}`** → `uniteLegale` **à plat** : `denominationUniteLegale`,\n * `nomUniteLegale`, `etatAdministratifUniteLegale`, etc. exposés directement\n * comme champs de l'objet `uniteLegale`, SANS `periodesUniteLegale`.\n *\n * On laisse les deux shapes coexister via `ApiInseePeriode & { periodesUniteLegale? }`\n * pour que `pickUniteLegaleFields` puisse extraire les champs courants quelle\n * que soit la source. Avant V0.6.3, le mapper lisait uniquement\n * `periodesUniteLegale[]` → la réponse `/siret/` (champs à plat) tombait sur\n * un tableau vide, et `deriveNomComplet` retournait le SIREN brut comme\n * raison sociale (cas reproduit sur 50781594200333 / BIOGROUP NORD →\n * \"507815942\", 30116075000966 / CLINEA → \"301160750\").\n */\ntype ApiInseeUniteLegale = ApiInseePeriode & {\n siren?: string;\n periodesUniteLegale?: ApiInseePeriode[];\n};\n\n/**\n * Extrait les champs métier courants (denomination, nom, prénom, état admin,\n * NAF, catégorie juridique) d'un `uniteLegale` SIRENE quelle que soit la shape :\n *\n * - Si `periodesUniteLegale` est présent ET non vide → période courante\n * (`dateFin: null`) ou fallback `[0]`.\n * - Sinon → l'objet `uniteLegale` lui-même (cas `/siret/` champs à plat).\n *\n * Retourne `undefined` si aucune source de champs n'est disponible (payload\n * dégradé). Le caller (`deriveNomComplet`) tombe alors sur le SIREN brut.\n */\nfunction pickUniteLegaleFields(ul: ApiInseeUniteLegale | undefined): ApiInseePeriode | undefined {\n if (!ul) return undefined;\n const periodes = ul.periodesUniteLegale;\n if (periodes && periodes.length > 0) {\n return periodes.find((p) => p.dateFin === null || p.dateFin === undefined) ?? periodes[0];\n }\n // Destructure explicite pour ne renvoyer que les champs `ApiInseePeriode` :\n // évite que `siren` / `periodesUniteLegale` (extras du type union) ne\n // fuitent en aval et fasse croire au caller qu'il a un objet plus riche.\n const { siren: _siren, periodesUniteLegale: _periodes, ...periodeFields } = ul;\n return periodeFields;\n}\n\ntype ApiInseeResponse = {\n uniteLegale?: ApiInseeUniteLegale;\n};\n\n/**\n * Récupère une entreprise par SIREN via l'API SIRENE INSEE V3.11.\n *\n * Comportement :\n * - Pas de clé configurée → `null` (no-op gracieux, pas de throw)\n * - HTTP 404 → `null` (vraiment pas dans SIRENE)\n * - HTTP 401/403 → `null` + `console.error` (clé invalide ou révoquée)\n * - HTTP 5xx / timeout / erreur réseau → `null` + `console.error`\n * - HTTP 200 → `Entreprise` mappée minimale (siren, nomComplet, naf, actif)\n *\n * ⚠️ Rate limit INSEE : 30 req/min. `fetchJson` retry sur 429 en respectant\n * `retry-after`, mais en burst soutenu (>30 lookups/min) les retries se\n * sérialisent et la latence p99 explose. Conçu comme fallback ponctuel sur\n * SIREN diffusion partielle (cas rare ~1% des SIREN), pas comme source primaire.\n */\nexport async function lookupSirenViaInsee(siren: string): Promise<Entreprise | null> {\n const apiKey = getInseeApiKey();\n if (!apiKey) return null;\n\n // fetchJson gère retry exponentiel sur 5xx + retry-after sur 429 — important\n // sur INSEE qui rate-limit à 30 req/min. Les 4xx (404/401/403) throwent en\n // HttpError immédiat, qu'on attrape pour transformer en `null` (le contrat\n // de cette fonction est un fallback gracieux, pas une propagation d'erreur).\n const url = `${SIRENE_BASE_URL}/siren/${encodeURIComponent(siren)}`;\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n let data: ApiInseeResponse;\n try {\n data = await fetchJson<ApiInseeResponse>(url, {\n headers: { [INSEE_AUTH_HEADER]: apiKey },\n signal: controller.signal,\n });\n } catch (err) {\n // Log unique en début de catch (discipline error-handling : zéro silence,\n // un seul point de trace par catch). 404 = outcome attendu (`warn`, pour\n // ne pas polluer les dashboards d'erreurs Sentry/Vercel) ; 401/403/5xx/\n // network = vrais incidents (`error`).\n const httpStatus = err instanceof HttpError ? err.status : null;\n const errMsg = err instanceof Error ? err.message : String(err);\n const logFn = httpStatus === 404 ? console.warn : console.error;\n logFn(\n `[france-data-mcp] INSEE SIRENE lookup terminated for siren=${siren} — ${httpStatus !== null ? `HTTP ${httpStatus}` : `network/parse error: ${errMsg}`}`,\n );\n return null;\n } finally {\n clearTimeout(timeout);\n }\n\n const ul = data.uniteLegale;\n if (!ul) {\n console.warn(`[france-data-mcp] INSEE SIRENE response missing uniteLegale for siren=${siren}`);\n return null;\n }\n\n // Période courante = celle dont `dateFin` est null (= période ouverte). On\n // ne se fie pas à l'ordre du tableau (l'API V3.11 le présente\n // antéchronologiquement aujourd'hui mais ce n'est pas un contrat documenté ;\n // un futur INSEE V3.12 pourrait l'inverser sans préavis). Fallback sur [0]\n // si aucune période ouverte (cas dégénéré : entreprise cessée, données\n // historiques uniquement) — on log alors un warn pour signaler la dépendance.\n const periodes = ul.periodesUniteLegale ?? [];\n let periode = periodes.find((p) => p.dateFin === null || p.dateFin === undefined);\n if (!periode && periodes.length > 0) {\n periode = periodes[0];\n console.warn(\n `[france-data-mcp] INSEE SIRENE siren=${siren} : aucune période ouverte (dateFin=null), fallback sur periodesUniteLegale[0] (potentiellement obsolète)`,\n );\n }\n\n return {\n siren,\n nomComplet: deriveNomComplet(periode, siren),\n finances: [],\n dirigeants: [],\n actif: periode?.etatAdministratifUniteLegale === \"A\",\n etablissements: [],\n enrichmentStatus: \"not_attempted\",\n siren_source: \"insee_v3\",\n ...(periode?.activitePrincipaleUniteLegale\n ? { naf: periode.activitePrincipaleUniteLegale }\n : {}),\n ...(periode?.categorieJuridiqueUniteLegale\n ? { natureJuridique: periode.categorieJuridiqueUniteLegale }\n : {}),\n };\n}\n\n/**\n * Reconstruit `nomComplet` depuis la période courante :\n * - `denominationUniteLegale` (raison sociale) prime quand présente (personnes morales)\n * - sinon `prenom + nom` (entrepreneur individuel) — `prenomUsuelUniteLegale` est\n * le champ canonique, `prenom1UniteLegale` est un fallback historique\n * - sinon `siren` brut (signal explicite que la donnée nominative manque)\n */\nfunction deriveNomComplet(periode: ApiInseePeriode | undefined, siren: string): string {\n if (!periode) return siren;\n const denomination = periode.denominationUniteLegale?.trim();\n if (denomination) return denomination;\n const prenom = (periode.prenomUsuelUniteLegale ?? periode.prenom1UniteLegale)?.trim();\n const nom = periode.nomUniteLegale?.trim();\n if (prenom && nom) return `${prenom} ${nom}`;\n if (nom) return nom;\n return siren;\n}\n\n// === Établissement (SIRET) lookup ============================================\n\n/**\n * Détail d'un établissement SIRENE retourné par `lookupSiretViaInsee`. Distinct\n * de `Etablissement` (dinum.ts) qui ne contient qu'une vue partielle obtenue\n * dans un résultat de recherche par SIREN. Ce type expose en plus :\n *\n * - `enseigne` et `denominationUsuelle` (commerciales — souvent l'enseigne\n * visible publiquement vs la raison sociale légale de l'unité légale)\n * - `dateFermeture` (présent quand `actif === false`)\n * - `raisonSocialeUniteLegale` (parent SIREN)\n *\n * Pas de `coords` : l'endpoint INSEE `/siret/<siret>` ne renvoie pas les coords\n * WGS84. Pour la géoloc, croiser avec `entreprises_in_radius` ou géocoder\n * l'adresse côté caller via `geocode_adresse`.\n */\nexport interface EtablissementSireneDetail {\n siret: string;\n siren: string;\n /** Raison sociale légale de l'unité légale parente. */\n raisonSocialeUniteLegale: string;\n /** Enseigne commerciale 1 (souvent affichée en façade). */\n enseigne: string | null;\n /** Dénomination usuelle (alias enseigne, parfois distinct). */\n denominationUsuelle: string | null;\n /** Code NAF de l'établissement (peut différer du NAF de l'unité légale). */\n naf: string | null;\n /** `true` si la période courante est `etatAdministratifEtablissement = 'A'`. */\n actif: boolean;\n /** Date de création (première `dateDebut` chronologique). */\n dateCreation: string | null;\n /**\n * Date de fermeture (dernière `dateDebut` quand `etatAdministratifEtablissement\n * = 'F'`). `null` si l'établissement est actif. C'est cette info qui débloque\n * la détection d'un SIRET fermé encore listé comme actif côté FINESS (DREES\n * a 1-2 mois de retard sur la cessation effective).\n */\n dateFermeture: string | null;\n /**\n * `true` si ce SIRET est le siège de l'unité légale. L'endpoint INSEE\n * `/siret/<siret>` n'expose PAS le SIRET du siège quand on consulte un\n * établissement secondaire — pour récupérer le siège, appeler\n * `entreprise_by_siren(siren)` qui retourne `siretSiege` côté unité légale.\n */\n estSiege: boolean;\n /** Tranche d'effectif salarié (codes INSEE 00..53). */\n trancheEffectif: string | null;\n adresse: {\n /** Adresse complète assemblée (numéro + voie + CP + ville). */\n libelle: string;\n numeroVoie: string | null;\n typeVoie: string | null;\n libelleVoie: string | null;\n codePostal: string | null;\n libelleCommune: string | null;\n codeCommune: string | null;\n };\n}\n\ntype ApiInseePeriodeEtablissement = {\n dateDebut?: string | null;\n dateFin?: string | null;\n etatAdministratifEtablissement?: string | null;\n enseigne1Etablissement?: string | null;\n denominationUsuelleEtablissement?: string | null;\n activitePrincipaleEtablissement?: string | null;\n};\n\ntype ApiInseeAdresseEtablissement = {\n numeroVoieEtablissement?: string | null;\n typeVoieEtablissement?: string | null;\n libelleVoieEtablissement?: string | null;\n codePostalEtablissement?: string | null;\n libelleCommuneEtablissement?: string | null;\n codeCommuneEtablissement?: string | null;\n};\n\ntype ApiInseeEtablissement = {\n siren?: string;\n siret?: string;\n etablissementSiege?: boolean;\n trancheEffectifsEtablissement?: string | null;\n uniteLegale?: ApiInseeUniteLegale;\n adresseEtablissement?: ApiInseeAdresseEtablissement;\n periodesEtablissement?: ApiInseePeriodeEtablissement[];\n};\n\ntype ApiInseeSiretResponse = {\n etablissement?: ApiInseeEtablissement;\n};\n\n/**\n * Récupère un établissement par son SIRET via l'API SIRENE INSEE V3.11.\n *\n * Comportement contractuel (aligné sur `getEntrepriseBySiren` côté wrapper) :\n * - Pas de clé `INSEE_SIRENE_API_KEY` configurée → `LookupResult` not_found\n * avec message orientant le caller vers la config. Pas de throw : le tool\n * MCP doit rester appelable même sans clé INSEE (pour ne pas casser les\n * tools qui ne dépendent pas d'INSEE).\n * - HTTP 404 → `LookupResult` not_found (SIRET vraiment absent SIRENE)\n * - HTTP 401/403/5xx/timeout → throw (le caller décide quoi faire)\n * - HTTP 200 mais payload incohérent → throw\n *\n * @param siret 14 chiffres. Validation côté caller via le tool MCP.\n */\nexport async function lookupSiretViaInsee(\n siret: string,\n): Promise<LookupResult<EtablissementSireneDetail>> {\n const raw = await fetchSiretRawFromInsee(siret);\n if (raw.kind !== \"ok\") return raw.lookup;\n return lookupFound(toEtablissementSireneDetail(raw.etablissement));\n}\n\n/**\n * Récupère l'historique complet (toutes les périodes) d'un établissement SIRET\n * via SIRENE INSEE V3.11. Permet de reconstruire la timeline ouvert/fermé\n * d'un site et de détecter une fermeture encore listée active côté FINESS.\n *\n * Contractuellement aligné sur `lookupSiretViaInsee` : retourne `LookupResult`,\n * pas de clé INSEE → not_found avec message, etc.\n */\nexport async function lookupSiretHistoriqueViaInsee(\n siret: string,\n): Promise<LookupResult<EtablissementSireneHistorique>> {\n const raw = await fetchSiretRawFromInsee(siret);\n if (raw.kind !== \"ok\") return raw.lookup;\n const detail = toEtablissementSireneDetail(raw.etablissement);\n const periodes = (raw.etablissement.periodesEtablissement ?? [])\n .map(toPeriodeHistorique)\n // Ordre chronologique croissant (la plus ancienne en premier). Plus\n // lisible pour un caller LLM qui lit la timeline en séquence.\n .sort((a, b) => (a.dateDebut ?? \"\").localeCompare(b.dateDebut ?? \"\"));\n return lookupFound({ ...detail, periodes });\n}\n\n/**\n * Période historique mappée. Volontairement compacte : on garde uniquement\n * les champs qui changent au fil des changements administratifs (état, NAF,\n * enseigne). Le caller LLM peut ainsi lire la timeline sans noise.\n */\nexport interface PeriodeHistorique {\n dateDebut: string | null;\n dateFin: string | null;\n actif: boolean;\n naf: string | null;\n enseigne: string | null;\n denominationUsuelle: string | null;\n}\n\nexport interface EtablissementSireneHistorique extends EtablissementSireneDetail {\n periodes: PeriodeHistorique[];\n}\n\nfunction toPeriodeHistorique(p: ApiInseePeriodeEtablissement): PeriodeHistorique {\n return {\n dateDebut: p.dateDebut ?? null,\n dateFin: p.dateFin ?? null,\n actif: p.etatAdministratifEtablissement === \"A\",\n naf: p.activitePrincipaleEtablissement ?? null,\n enseigne: p.enseigne1Etablissement?.trim() || null,\n denominationUsuelle: p.denominationUsuelleEtablissement?.trim() || null,\n };\n}\n\n/**\n * Discriminated union : soit la requête INSEE a réussi (`ok` + payload), soit\n * elle est terminée par un `LookupResult` not_found (clé absente, 404,\n * payload incohérent). Les vrais incidents (401/5xx/timeout) sont propagés\n * via throw et n'arrivent pas ici.\n */\ntype FetchSiretRawResult =\n | { kind: \"ok\"; etablissement: ApiInseeEtablissement }\n | { kind: \"lookup\"; lookup: LookupResult<never> };\n\nasync function fetchSiretRawFromInsee(siret: string): Promise<FetchSiretRawResult> {\n const apiKey = getInseeApiKey();\n if (!apiKey) {\n return {\n kind: \"lookup\",\n lookup: lookupNotFound(\n siret,\n \"INSEE_SIRENE_API_KEY non configurée — ce tool requiert une clé INSEE pour interroger l'endpoint /siret/<siret> de l'API SIRENE V3.11. Inscription gratuite : https://api.insee.fr/catalogue/. Une fois la clé obtenue, définir la variable d'env INSEE_SIRENE_API_KEY sur le déploiement.\",\n ),\n };\n }\n\n const url = `${SIRENE_BASE_URL}/siret/${encodeURIComponent(siret)}`;\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n let data: ApiInseeSiretResponse;\n try {\n data = await fetchJson<ApiInseeSiretResponse>(url, {\n headers: { [INSEE_AUTH_HEADER]: apiKey },\n signal: controller.signal,\n });\n } catch (err) {\n const httpStatus = err instanceof HttpError ? err.status : null;\n if (httpStatus === 404) {\n console.warn(`[france-data-mcp] INSEE SIRENE SIRET ${siret} — HTTP 404 (introuvable)`);\n return {\n kind: \"lookup\",\n lookup: lookupNotFound(\n siret,\n `SIRET \"${siret}\" introuvable dans SIRENE INSEE. Causes possibles : SIRET inexistant, erreur de saisie, ou statut de diffusion partielle INSEE (rare). Pour vérifier la diffusion, croiser avec entreprise_by_siren.`,\n ),\n };\n }\n // Vrais incidents : 401/403/5xx/timeout/réseau. On les propage pour que\n // le caller puisse retry ou alerter, plutôt que masquer en `not_found`.\n const errMsg = err instanceof Error ? err.message : String(err);\n console.error(\n `[france-data-mcp] INSEE SIRENE SIRET ${siret} — ${httpStatus !== null ? `HTTP ${httpStatus}` : `network/parse error: ${errMsg}`}`,\n );\n throw err;\n } finally {\n clearTimeout(timeout);\n }\n\n const et = data.etablissement;\n if (!et || !et.siret || !et.siren) {\n console.warn(\n `[france-data-mcp] INSEE SIRENE SIRET ${siret} — payload incohérent (etablissement, siret ou siren absent)`,\n );\n return {\n kind: \"lookup\",\n lookup: lookupNotFound(\n siret,\n `Réponse INSEE incohérente pour SIRET \"${siret}\" (etablissement absent ou champs critiques manquants). Réessayer plus tard ou signaler.`,\n ),\n };\n }\n\n return { kind: \"ok\", etablissement: et };\n}\n\n// === Lookup établissements par SIREN (endpoint /siret?q=siren:XXX) ===========\n\n/**\n * Shape de la réponse de l'endpoint de recherche `GET /siret?q=siren:{siren}`.\n *\n * Distinct de `ApiInseeSiretResponse` (qui mappe l'endpoint `/siret/{siret}`) :\n * ici le top-level est un tableau `etablissements[]` + une section `header`\n * avec les métadonnées de pagination.\n */\ntype ApiInseeSearchSiretResponse = {\n header?: {\n statut?: number;\n message?: string;\n total?: number;\n debut?: number;\n nombre?: number;\n };\n etablissements?: ApiInseeEtablissement[];\n};\n\n/**\n * Récupère la liste de **tous les établissements** d'un SIREN via l'endpoint\n * de recherche SIRENE V3.11 `GET /siret?q=siren:{siren}&nombre=1000`.\n *\n * Pourquoi : `getEntrepriseBySiren` (DINUM) retourne `enrichmentStatus: \"partial\"`\n * pour les SIREN multi-sites (≥ ~20 établissements) et n'expose que le siège.\n * Ce helper comble le manque en interrogeant SIRENE directement — il est appelé\n * en fallback par `resolveSiretsForFiness` uniquement quand DINUM est partial.\n *\n * Comportement contractuel :\n * - Pas de clé `INSEE_SIRENE_API_KEY` → `LookupResult` not_found avec message\n * explicatif orientant le caller vers la config. Pas de throw (no-op gracieux).\n * - HTTP 404 → `LookupResult` not_found (SIREN absent SIRENE)\n * - HTTP 401/403/5xx/timeout → throw (incident, le caller gère via graceful degradation)\n * - Pagination : charge la première page (1000 items). Si `header.total > 1000`,\n * émet un `console.warn` pour signaler que des établissements sont tronqués\n * (V0.7.2 gèrera la pagination complète le cas échéant).\n *\n * @param siren 9 chiffres. Validé côté caller.\n */\nexport async function lookupSiretsBySirenViaInsee(\n siren: string,\n): Promise<LookupResult<{ etablissements: EtablissementSireneDetail[] }>> {\n const apiKey = getInseeApiKey();\n if (!apiKey) {\n return lookupNotFound(\n siren,\n \"INSEE_SIRENE_API_KEY non configurée — fallback INSEE désactivé. Pour activer la résolution des SIREN multi-sites (enrichmentStatus=partial DINUM), définir INSEE_SIRENE_API_KEY (clé gratuite : https://api.insee.fr/catalogue/).\",\n );\n }\n\n // L'endpoint de recherche attend des paramètres de query (`q` avec syntaxe\n // Solr-like), contrairement à `/siret/{siret}` qui est un lookup direct.\n // `nombre=1000` = taille de page maximale documentée (V3.11 — largement\n // suffisant pour les SIREN les plus multi-sites, ex: Biogroup = 38 sites).\n const url = `${SIRENE_BASE_URL}/siret?q=siren:${encodeURIComponent(siren)}&nombre=1000`;\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n let data: ApiInseeSearchSiretResponse;\n try {\n data = await fetchJson<ApiInseeSearchSiretResponse>(url, {\n headers: { [INSEE_AUTH_HEADER]: apiKey },\n signal: controller.signal,\n });\n } catch (err) {\n const httpStatus = err instanceof HttpError ? err.status : null;\n if (httpStatus === 404) {\n console.warn(\n `[france-data-mcp] INSEE SIRENE fallback: siren=${siren} — HTTP 404 (SIREN absent SIRENE)`,\n );\n return lookupNotFound(\n siren,\n `SIREN \"${siren}\" introuvable via l'endpoint de recherche SIRENE (HTTP 404). Causes possibles : SIREN inexistant ou statut de diffusion partielle totale.`,\n );\n }\n // Incidents vrais (401/403/5xx/timeout/réseau) : on propage pour que\n // le caller `resolveSiretsForFiness` puisse les capturer dans `dinum_errors`\n // avec status \"rejected\" — cohérent avec la convention V0.6.x.\n const errMsg = err instanceof Error ? err.message : String(err);\n console.error(\n `[france-data-mcp] INSEE SIRENE fallback: siren=${siren} — ${httpStatus !== null ? `HTTP ${httpStatus}` : `network/parse error: ${errMsg}`}`,\n );\n throw err;\n } finally {\n clearTimeout(timeout);\n }\n\n const rawEtabs = data.etablissements ?? [];\n if (rawEtabs.length === 0) {\n console.warn(\n `[france-data-mcp] INSEE SIRENE fallback: siren=${siren} — réponse vide (0 établissements)`,\n );\n return lookupNotFound(\n siren,\n `Aucun établissement trouvé côté SIRENE pour siren=\"${siren}\" (liste vide). SIREN absent ou diffusion partielle totale.`,\n );\n }\n\n // Signal de troncation : si header.total > nombre de résultats retournés,\n // des établissements sont hors page 1. Rare en prod (Biogroup = 38, max\n // connu ~200 pour les grandes chaînes) mais un SIREN avec >1000 SIRET\n // (ex: grande distribution, banque) déclencherait un warn ici.\n const total = data.header?.total ?? rawEtabs.length;\n if (total > 1000) {\n console.warn(\n `[france-data-mcp] INSEE SIRENE fallback: siren=${siren} — header.total=${total} > 1000 (page 1 tronquée, V0.7.2 pagination requise)`,\n );\n }\n\n return lookupFound({ etablissements: rawEtabs.map(toEtablissementSireneDetail) });\n}\n\n/**\n * Convertit un payload `ApiInseeEtablissement` brut en `EtablissementSireneDetail`.\n * Helper interne réutilisé par `lookupSiretViaInsee`, `lookupSiretHistoriqueViaInsee`\n * et `lookupSiretsBySirenViaInsee`.\n */\nfunction toEtablissementSireneDetail(api: ApiInseeEtablissement): EtablissementSireneDetail {\n const periodes = api.periodesEtablissement ?? [];\n // Période courante = `dateFin === null`. L'API présente antéchronologiquement\n // mais on ne s'y fie pas (cf. note dans `lookupSirenViaInsee`).\n const periodeCourante = periodes.find((p) => p.dateFin === null || p.dateFin === undefined);\n // `actif` ne se déduit PAS de l'existence d'une période courante : un\n // établissement fermé garde une période courante avec `etatAdministratif = 'F'`.\n const actif = periodeCourante?.etatAdministratifEtablissement === \"A\";\n\n // dateCreation : la `dateDebut` la plus ancienne (la première période, en\n // ordre chronologique). dateFermeture : la `dateDebut` de la période\n // courante quand elle est 'F' (= date du basculement actif → fermé).\n const periodesChrono = [...periodes].sort((a, b) =>\n (a.dateDebut ?? \"\").localeCompare(b.dateDebut ?? \"\"),\n );\n const dateCreation = periodesChrono[0]?.dateDebut ?? null;\n const dateFermeture = !actif ? (periodeCourante?.dateDebut ?? null) : null;\n\n // Raison sociale de l'unité légale parente : SIRENE V3.11 expose les champs\n // de l'uniteLegale À PLAT sur l'endpoint `/siret/` (pas dans\n // `periodesUniteLegale[]`, contrairement à `/siren/`). `pickUniteLegaleFields`\n // gère les deux shapes pour rester robuste si V3.12 réintroduit l'historisation.\n const raisonSocialeUniteLegale = deriveNomComplet(\n pickUniteLegaleFields(api.uniteLegale),\n api.siren ?? \"\",\n );\n\n const a = api.adresseEtablissement ?? {};\n const adresseLibelle = [\n a.numeroVoieEtablissement,\n a.typeVoieEtablissement,\n a.libelleVoieEtablissement,\n a.codePostalEtablissement,\n a.libelleCommuneEtablissement,\n ]\n .filter((p): p is string => typeof p === \"string\" && p.trim().length > 0)\n .join(\" \")\n .trim();\n\n return {\n siret: api.siret ?? \"\",\n siren: api.siren ?? \"\",\n raisonSocialeUniteLegale,\n enseigne: periodeCourante?.enseigne1Etablissement?.trim() || null,\n denominationUsuelle: periodeCourante?.denominationUsuelleEtablissement?.trim() || null,\n naf: periodeCourante?.activitePrincipaleEtablissement ?? null,\n actif,\n dateCreation,\n dateFermeture,\n estSiege: api.etablissementSiege === true,\n trancheEffectif: api.trancheEffectifsEtablissement ?? null,\n adresse: {\n libelle: adresseLibelle,\n numeroVoie: a.numeroVoieEtablissement ?? null,\n typeVoie: a.typeVoieEtablissement ?? null,\n libelleVoie: a.libelleVoieEtablissement ?? null,\n codePostal: a.codePostalEtablissement ?? null,\n libelleCommune: a.libelleCommuneEtablissement ?? null,\n codeCommune: a.codeCommuneEtablissement ?? null,\n },\n };\n}\n","/**\n * DINUM Recherche Entreprises — annuaire des entreprises françaises (SIRENE + RNE).\n *\n * URL : https://recherche-entreprises.api.gouv.fr/search\n * Doc : https://recherche-entreprises.api.gouv.fr/docs/\n *\n * Rate limit documenté : 7 req/s. Observé en pratique : ~1 req/s effectif après le\n * premier 429 (header retry-after: 4 systématique). Le helper fetchJson respecte\n * retry-after, mais ne pas dépasser 1 req/s en burst pour éviter les 429 répétés.\n *\n * Sans clé API, sans authentification, CORS autorisé.\n *\n * Données utiles côté santé : le filtre `activite_principale` (NAF) permet de\n * cibler labos (8690B), pharmacies (4773Z), maisons médicales (8621Z), SSR\n * (8610Z), EHPAD (8710A), centres médico-sociaux, etc.\n */\n\nimport { parseCoordinates } from \"../core/coords.js\";\nimport { fetchJson } from \"../core/http.js\";\nimport { type LookupResult, lookupFound, lookupNotFound } from \"../core/lookup-result.js\";\nimport { clamp } from \"../core/numbers.js\";\nimport { pickDefined } from \"../core/object-utils.js\";\nimport type { Coordinates } from \"../core/types.js\";\nimport { getInseeApiKey, lookupSirenViaInsee } from \"./insee-sirene.js\";\n\nconst BASE_URL = \"https://recherche-entreprises.api.gouv.fr/search\";\n\nexport type Etablissement = {\n /** SIRET 14 chiffres */\n siret: string;\n /** Adresse complète */\n adresse: string;\n /** Code postal */\n codePostal?: string;\n /** Commune */\n commune?: string;\n /** Coordonnées GPS si l'adresse est géocodée */\n point?: Coordinates;\n /** Code NAF de l'établissement */\n naf?: string;\n /** Établissement actif administrativement (etat_administratif === \"A\") */\n actif: boolean;\n /** Tranche d'effectif salarié (codes INSEE 0..53) */\n trancheEffectif?: string;\n /** Date de création de l'établissement */\n dateCreation?: string;\n};\n\nexport type Finance = {\n annee: number;\n ca?: number;\n resultatNet?: number;\n /**\n * Signal de fiabilité du `ca`. `false` quand `ca===0` ET `resultatNet>0` :\n * pattern observé à 100% sur les SELARL pharma (NAF 47.73Z) qui ne déclarent\n * pas leur CA au RNE — il ne faut pas l'afficher comme un vrai 0. Vraie\n * dormance (`resultatNet<=0` ou undefined) reste `caFiable: true`.\n */\n caFiable: boolean;\n};\n\nexport type Dirigeant = {\n nom?: string;\n prenoms?: string;\n fonction?: string;\n qualite?: string;\n};\n\n/**\n * État de l'enrichissement de la liste `etablissements` :\n *\n * - `not_attempted` : monosite (ou data SIRENE manquante) — pas de second appel.\n * - `success` : `etablissements.length === nombreEtablissements` (ou >=).\n * - `partial` : second appel OK mais retourne moins que le total SIRENE\n * (cause typique : entreprise multi-département ou NAF secondaires).\n * - `failed` : second appel a échoué (rate limit, panne API, parsing…).\n * `enrichmentWarning` contient le message d'erreur.\n */\nexport type EnrichmentStatus = \"not_attempted\" | \"success\" | \"partial\" | \"failed\";\n\n/** Source d'origine du lookup d'une `Entreprise`. Voir champ `siren_source` ci-dessous. */\nexport type EntrepriseSirenSource = \"dinum\" | \"insee_v3\";\n\nexport type Entreprise = {\n /** SIREN 9 chiffres */\n siren: string;\n /** SIRET du siège (14 chiffres) */\n siretSiege?: string;\n /** Nom complet (raison sociale ou nom + prénom pour entrepreneurs individuels) */\n nomComplet: string;\n /** Code NAF principal */\n naf?: string;\n /** Libellé NAF principal */\n nafLibelle?: string;\n /** Tranche d'effectif (CA / RN dans `finances`) */\n trancheEffectif?: string;\n /** Code juridique INSEE */\n natureJuridique?: string;\n /** Finances historiques par année (les plus récentes sont en premier) */\n finances: Finance[];\n /** Dirigeants déclarés au RNE */\n dirigeants: Dirigeant[];\n /**\n * Établissements actifs et inactifs.\n *\n * ⚠️ Pour `searchEntreprises({ q })` ou `getEntrepriseBySiren()`, ce champ peut\n * ne contenir que le siège — l'API DINUM ne retourne que les établissements\n * « matchant » la requête. `getEntrepriseBySiren()` fait un second appel\n * automatique pour récupérer les établissements du même NAF principal dans\n * le département du siège.\n *\n * **Le caller doit lire `enrichmentStatus`** pour savoir si la liste est\n * complète (`success`), tronquée (`partial`), ou si l'enrichissement a échoué\n * (`failed`). Comparer aussi `etablissements.length` à `nombreEtablissements`.\n */\n etablissements: Etablissement[];\n /** Nombre total d'établissements (actifs + fermés), source SIRENE */\n nombreEtablissements?: number;\n /** Nombre d'établissements actuellement ouverts, source SIRENE */\n nombreEtablissementsOuverts?: number;\n /**\n * État de l'enrichissement multi-sites (cf. `EnrichmentStatus`).\n * Toujours présent pour les retours de `getEntrepriseBySiren()`.\n * Absent pour les `searchEntreprises()` (pas d'enrichissement tenté).\n */\n enrichmentStatus?: EnrichmentStatus;\n /** Message d'aide quand `enrichmentStatus` ∈ {\"partial\", \"failed\"}. */\n enrichmentWarning?: string;\n /**\n * Source effective du lookup. `\"dinum\"` (défaut implicite) = retour de l'API\n * recherche-entreprises.api.gouv.fr. `\"insee_v3\"` = fallback SIRENE INSEE V3\n * activé quand DINUM ne connaît pas le SIREN (cas diffusion partielle). En\n * mode insee_v3, finances/dirigeants/etablissements sont vides — l'API\n * /siren/{siren} ne les expose pas sans appels supplémentaires /siret.\n */\n siren_source?: EntrepriseSirenSource;\n /** Statut administratif global */\n actif: boolean;\n};\n\nexport type SearchEntreprisesOptions = {\n /** Recherche textuelle (raison sociale, dirigeant…) */\n q?: string;\n /** Filtre exact sur le code NAF (ex: \"8690B\" pour labos d'analyses médicales) */\n naf?: string;\n /** Filtre par code postal */\n codePostal?: string;\n /** Filtre par département (code 2 ou 3 caractères) */\n departement?: string;\n /** Filtre par code commune INSEE */\n codeCommune?: string;\n /**\n * Recherche géographique : centre + rayon (km, max 50). DOIT être combiné\n * avec `q` (recherche textuelle) — l'API DINUM rejette `naf + lat/lon/radius`\n * directement. Pour combiner NAF + zone géographique, utiliser le tool MCP\n * `entreprises_in_radius` qui applique un fallback automatique\n * (reverseGeocode → département → filtre Haversine).\n */\n center?: Coordinates;\n /** Rayon en km (1-50). Requis si `center` est fourni. */\n radiusKm?: number;\n /** Limiter aux établissements administrativement actifs (défaut: true) */\n onlyActive?: boolean;\n /** Page de résultats (1-indexed, défaut 1) */\n page?: number;\n /** Résultats par page (1-25, défaut 10) */\n perPage?: number;\n signal?: AbortSignal;\n};\n\nexport type SearchEntreprisesResult = {\n total: number;\n page: number;\n perPage: number;\n totalPages: number;\n entreprises: Entreprise[];\n};\n\ntype ApiSiege = {\n siret?: string;\n adresse?: string;\n code_postal?: string;\n libelle_commune?: string;\n // L'API DINUM peut renvoyer `null` (pas seulement `undefined`) pour les\n // sites sans géocodage SIRENE. `parseCoordinates` ci-dessous accepte les\n // deux et renvoie `undefined` proprement.\n latitude?: string | number | null;\n longitude?: string | number | null;\n activite_principale?: string;\n etat_administratif?: string;\n tranche_effectif_salarie?: string;\n date_creation?: string;\n};\n\ntype ApiMatchingEt = ApiSiege;\n\ntype ApiFinances = Record<\n string,\n {\n ca?: number;\n resultat_net?: number;\n }\n>;\n\ntype ApiDirigeant = {\n nom?: string;\n prenoms?: string;\n fonction?: string;\n qualite?: string;\n};\n\ntype ApiEntreprise = {\n siren: string;\n nom_complet?: string;\n nom_raison_sociale?: string;\n activite_principale?: string;\n libelle_activite_principale?: string;\n nature_juridique?: string;\n tranche_effectif_salarie?: string;\n etat_administratif?: string;\n nombre_etablissements?: number;\n nombre_etablissements_ouverts?: number;\n finances?: ApiFinances;\n dirigeants?: ApiDirigeant[];\n siege?: ApiSiege;\n matching_etablissements?: ApiMatchingEt[];\n};\n\ntype ApiResponse = {\n results: ApiEntreprise[];\n total_results: number;\n page: number;\n per_page: number;\n total_pages: number;\n};\n\n/**\n * Recherche d'entreprises avec filtres NAF / géo / texte libre.\n *\n * @example Tous les labos de bio médicale dans 5 km autour d'un point\n * ```ts\n * const labos = await searchEntreprises({\n * naf: \"8690B\",\n * center: { lon: 4.7192, lat: 49.7672 },\n * radiusKm: 5,\n * });\n * ```\n *\n * @example Toutes les pharmacies du 08\n * ```ts\n * const pharma = await searchEntreprises({ naf: \"4773Z\", departement: \"08\" });\n * ```\n */\nexport async function searchEntreprises(\n options: SearchEntreprisesOptions,\n): Promise<SearchEntreprisesResult> {\n const {\n q,\n naf,\n codePostal,\n departement,\n codeCommune,\n center,\n radiusKm,\n onlyActive = true,\n page = 1,\n perPage = 10,\n signal,\n } = options;\n\n if (!q && !naf && !codePostal && !departement && !codeCommune && !center) {\n throw new Error(\n \"searchEntreprises: au moins un critère est requis (q, naf, codePostal, departement, codeCommune ou center+radiusKm)\",\n );\n }\n\n if (center && (radiusKm === undefined || radiusKm <= 0)) {\n throw new Error(\"searchEntreprises: radiusKm > 0 requis quand center est fourni\");\n }\n\n // L'API DINUM exige que `lat/long/radius` soient accompagnés d'un `q` (recherche\n // textuelle). On ne peut pas combiner `activite_principale` + lat/long/radius\n // directement. Si le caller fournit center+radiusKm sans q, on en injecte un\n // par défaut via le NAF si présent, sinon on signale l'incompatibilité.\n if (center && !q) {\n if (naf) {\n throw new Error(\n \"searchEntreprises: l'API DINUM n'accepte pas `naf` + `center+radiusKm` directement. \" +\n \"Options : (1) `q='<terme>'` + center+radiusKm (recherche textuelle géolocalisée), \" +\n \"(2) `naf` + `codePostal`/`departement`/`codeCommune` (filtrage administratif), \" +\n \"(3) faire un reverseGeocode du center pour obtenir codeCommune puis filtrer.\",\n );\n }\n throw new Error(\n \"searchEntreprises: `center+radiusKm` requiert un paramètre `q` (recherche textuelle). \" +\n \"L'API DINUM ne supporte pas la recherche géographique pure.\",\n );\n }\n\n const params = new URLSearchParams();\n if (q) params.set(\"q\", q);\n if (naf) params.set(\"activite_principale\", normalizeNafCode(naf));\n if (codePostal) params.set(\"code_postal\", codePostal);\n if (departement) params.set(\"departement\", departement);\n if (codeCommune) params.set(\"code_commune\", codeCommune);\n if (center && radiusKm !== undefined) {\n params.set(\"lat\", String(center.lat));\n params.set(\"long\", String(center.lon));\n params.set(\"radius\", String(Math.min(radiusKm, 50)));\n }\n if (onlyActive) params.set(\"etat_administratif\", \"A\");\n params.set(\"page\", String(Math.max(1, page)));\n params.set(\"per_page\", String(clamp(perPage, 1, 25)));\n\n const url = `${BASE_URL}?${params.toString()}`;\n const data = await fetchJson<ApiResponse>(url, { signal });\n\n return {\n total: data.total_results,\n page: data.page,\n perPage: data.per_page,\n totalPages: data.total_pages,\n entreprises: data.results.map(toEntreprise),\n };\n}\n\n/**\n * Récupère une entreprise par son SIREN (9 chiffres).\n * Renvoie null si introuvable. Throw si l'API DINUM est en panne ou rate-limit dépassé.\n *\n * Implémentation :\n * 1. `q=<siren>` (l'API DINUM matche le SIREN dans le full-text), filtrage côté\n * client sur l'égalité exacte du SIREN.\n * 2. **Limitation API DINUM** : `q=<siren>` ne retourne que le siège dans\n * `matching_etablissements`. Pour récupérer les autres établissements, on\n * fait un second appel `activite_principale=<naf>&departement=<dept_siège>`\n * qui retourne tous les établissements de l'entreprise ayant le NAF\n * principal dans le département du siège (couvre la majorité des\n * multi-sites). Les `etablissements` du résultat fusionnent siège + ces\n * établissements supplémentaires (déduplication par SIRET).\n * 3. `nombreEtablissements` / `nombreEtablissementsOuverts` reflètent toujours\n * le total réel SIRENE (non limité par l'API DINUM).\n *\n * **Limitation indexation DINUM** : certaines entreprises pourtant actives à\n * l'INSEE/SIRENE ne sont PAS indexées par `recherche-entreprises.api.gouv.fr`\n * (statut de diffusion partielle au sens INSEE — `statut_diffusion ∈ {P,N}` —\n * ou exclusion sectorielle/légale). Ces SIREN reviennent `null` ici alors\n * qu'ils existent réellement. L'audit post-v0.2.0 a vérifié ce comportement\n * sur le SIREN 787120435 (Bio Ard'Aisne, SAS Rethel) : présent dans SIRENE\n * via la \"fabrique social.gouv\" mais absent de l'API DINUM publique.\n * Pour ce cas d'usage, fallback : interroger SIRENE INSEE directement (avec\n * authentification API), ou utiliser `entreprises_in_radius` par zone géo.\n */\nexport async function getEntrepriseBySiren(\n siren: string,\n signal?: AbortSignal,\n): Promise<LookupResult<Entreprise>> {\n if (!/^\\d{9}$/.test(siren)) {\n throw new Error(`getEntrepriseBySiren: SIREN invalide \"${siren}\" (attendu 9 chiffres)`);\n }\n const result = await searchEntreprises({ q: siren, perPage: 5, onlyActive: false, signal });\n const match = result.entreprises.find((e) => e.siren === siren);\n if (!match) {\n if (result.entreprises.length > 0) {\n // L'API a renvoyé des résultats mais aucun ne matche le SIREN exact —\n // bizarre, peut signaler une régression côté DINUM (recherche full-text\n // qui matche sur autre chose que le SIREN). À surveiller.\n console.warn(\n `[france-data-mcp] getEntrepriseBySiren(${siren}): l'API a renvoyé ${result.entreprises.length} résultat(s) sans match exact du SIREN.`,\n );\n return lookupNotFound(\n siren,\n `L'API DINUM a renvoyé ${result.entreprises.length} résultat(s) full-text mais aucun ne correspond exactement au SIREN ${siren}. Possible régression côté API DINUM ou faux positif full-text.`,\n \"ambiguous\",\n );\n }\n // \"pas indexé par DINUM\" ≠ \"n'existe pas dans SIRENE\". Comportement\n // normal pour les SIREN en diffusion partielle (cf. JSDoc ci-dessus, cas\n // Bio Ard'Aisne). On tente le fallback SIRENE INSEE V3 si configuré.\n const inseeMatch = await lookupSirenViaInsee(siren);\n if (inseeMatch) return lookupFound(inseeMatch);\n const inseeSuffix = getInseeApiKey()\n ? \"Fallback SIRENE INSEE V3 a aussi retourné null (SIREN absent de SIRENE, clé révoquée, ou panne API — voir logs).\"\n : \"Fallback SIRENE INSEE V3 non configuré (env var INSEE_SIRENE_API_KEY absente).\";\n return lookupNotFound(\n siren,\n `SIREN ${siren} non trouvé via DINUM (statut diffusion partielle probable). ${inseeSuffix}`,\n );\n }\n\n // Trouve le siège : on préfère le SIRET déclaré comme siège plutôt que\n // l'index 0 du tableau (l'ordre n'est pas garanti et peut changer si on\n // ré-ordonne plus tard).\n const siege =\n match.etablissements.find((e) => e.siret === match.siretSiege) ?? match.etablissements[0];\n const siegePostalCode = siege?.codePostal;\n const departement = deptFromPostal(siegePostalCode);\n const naf = match.naf;\n const totalSirene = match.nombreEtablissements ?? 0;\n\n if (totalSirene <= 1) {\n match.enrichmentStatus = \"not_attempted\";\n return lookupFound(match);\n }\n if (!naf || !departement) {\n match.enrichmentStatus = \"not_attempted\";\n match.enrichmentWarning = warnSkipped({ naf, siegePostalCode, departement });\n return lookupFound(match);\n }\n\n // Second appel : l'API DINUM expose les autres établissements dans\n // `matching_etablissements` uniquement quand on filtre par NAF + département.\n // ⚠️ Coût : ce wrapper consomme 2 appels DINUM par invocation pour les\n // multi-sites (rate limit observé ~1 req/s effectif après 429). Throttler\n // côté caller en cas de batch.\n try {\n const more = await searchEntreprises({\n naf,\n departement,\n perPage: 25,\n onlyActive: false,\n signal,\n });\n const enriched = more.entreprises.find((e) => e.siren === siren);\n if (enriched) {\n const seen = new Set(match.etablissements.map((e) => e.siret));\n for (const et of enriched.etablissements) {\n if (et.siret && !seen.has(et.siret)) {\n match.etablissements.push(et);\n seen.add(et.siret);\n }\n }\n }\n\n if (match.etablissements.length >= totalSirene) {\n match.enrichmentStatus = \"success\";\n } else {\n match.enrichmentStatus = \"partial\";\n match.enrichmentWarning = warnPartial({\n found: match.etablissements.length,\n totalSirene,\n naf,\n departement,\n });\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const errType = err instanceof Error ? err.constructor.name : typeof err;\n console.error(\n `[france-data-mcp] getEntrepriseBySiren(${siren}): échec enrichissement (errType=${errType}, naf=${naf}, departement=${departement}): ${msg}`,\n );\n match.enrichmentStatus = \"failed\";\n match.enrichmentWarning = warnFailed({ errType, msg, totalSirene });\n }\n\n return lookupFound(match);\n}\n\nfunction warnSkipped(opts: {\n naf: string | undefined;\n siegePostalCode: string | undefined;\n departement: string | undefined;\n}): string {\n return `Enrichissement ignoré (naf=${opts.naf ?? \"absent\"}, codePostal=${opts.siegePostalCode ?? \"absent\"}, departement=${opts.departement ?? \"non déductible\"}).`;\n}\n\nfunction warnPartial(opts: {\n found: number;\n totalSirene: number;\n naf: string;\n departement: string;\n}): string {\n return `Enrichissement partiel : ${opts.found}/${opts.totalSirene} établissements. Stratégie API DINUM (naf=${opts.naf} + departement=${opts.departement}) ne couvre pas les sites multi-département ni les établissements à NAF différent du siège. Pour exhaustivité : utiliser \\`entreprises_in_radius\\` par zone géographique, ou interroger SIRENE directement.`;\n}\n\nfunction warnFailed(opts: { errType: string; msg: string; totalSirene: number }): string {\n return `Enrichissement échoué (${opts.errType}: ${opts.msg}). nombreEtablissements=${opts.totalSirene} mais seul le siège est listé. Réessayer plus tard, ou utiliser \\`entreprises_in_radius\\` pour cibler géographiquement.`;\n}\n\n/**\n * Extrait le code département depuis un code postal français.\n *\n * Cas couverts :\n * - Métropole : `08000` → `\"08\"`, `75001` → `\"75\"`\n * - DOM (codes 971-978) : `97400` → `\"974\"`, `97600` → `\"976\"`\n * - TOM (codes 988) : `98800` → `\"988\"`\n * - Corse : `20100` → `\"2A\"` (Corse-du-Sud, 20000-20190),\n * `20200` → `\"2B\"` (Haute-Corse, 20200-20620)\n *\n * Note : différent de `deptFromCommune` (api/tools.ts) qui prend un code\n * commune INSEE (Corse `2A004` → `\"2A\"` directement). Ici on travaille sur\n * les codes postaux (Corse `20xxx`) qui demandent un mapping par plage.\n */\nfunction deptFromPostal(codePostal: string | undefined): string | undefined {\n if (!codePostal || codePostal.length < 2) return undefined;\n if (codePostal.startsWith(\"97\") || codePostal.startsWith(\"98\")) {\n return codePostal.length >= 3 ? codePostal.slice(0, 3) : undefined;\n }\n if (codePostal.startsWith(\"20\") && /^\\d{5}$/.test(codePostal)) {\n const n = Number.parseInt(codePostal, 10);\n if (n >= 20000 && n <= 20190) return \"2A\";\n if (n >= 20200 && n <= 20620) return \"2B\";\n return undefined;\n }\n return codePostal.slice(0, 2);\n}\n\n/**\n * Normalise un code NAF vers le format attendu par l'API DINUM (`XX.XXY`).\n *\n * L'API DINUM rejette les codes en format INSEE compact (`8690B`) avec un\n * HTTP 400 et la liste des valeurs valides. La nomenclature officielle utilise\n * des points (`86.90B`), donc on accepte les deux entrées et on convertit.\n *\n * On exige la lettre finale (`[A-Z]`) parce que les codes NAF sans lettre\n * (ex: `\"8690\"`) ne correspondent à aucune sous-classe valide — laisser passer\n * un tel code en le réécrivant `\"86.90\"` produirait toujours un 400, sans gain.\n */\nfunction normalizeNafCode(naf: string): string {\n // Déjà au format pointé : \"86.90B\"\n if (/^\\d{2}\\.\\d{2}[A-Z]$/.test(naf)) return naf;\n // Format compact : \"8690B\" → \"86.90B\"\n if (/^\\d{4}[A-Z]$/.test(naf)) return `${naf.slice(0, 2)}.${naf.slice(2)}`;\n // Format inconnu : on laisse passer, l'API renverra une erreur claire\n return naf;\n}\n\nfunction toEntreprise(api: ApiEntreprise): Entreprise {\n const finances: Finance[] = [];\n if (api.finances) {\n for (const [year, fin] of Object.entries(api.finances)) {\n const annee = Number.parseInt(year, 10);\n if (Number.isFinite(annee)) {\n // caFiable: false uniquement quand `ca === 0 && resultatNet > 0` —\n // signal \"non déclaré DINUM/RNE\" (audit SELARL pharma 2026-05-09). Si\n // ca est undefined OU resultatNet est <= 0/undefined, on considère\n // l'absence ou le 0 comme fiable (entreprise dormante plausible).\n const caFiable = !(fin.ca === 0 && fin.resultat_net !== undefined && fin.resultat_net > 0);\n const f: Finance = { annee, caFiable };\n if (fin.ca !== undefined) f.ca = fin.ca;\n if (fin.resultat_net !== undefined) f.resultatNet = fin.resultat_net;\n finances.push(f);\n }\n }\n finances.sort((a, b) => b.annee - a.annee);\n }\n\n const etablissements: Etablissement[] = [];\n if (api.siege) etablissements.push(toEtablissement(api.siege));\n if (api.matching_etablissements) {\n for (const m of api.matching_etablissements) {\n if (!api.siege || m.siret !== api.siege.siret) {\n etablissements.push(toEtablissement(m));\n }\n }\n }\n\n const entreprise: Entreprise = {\n siren: api.siren,\n nomComplet: api.nom_complet ?? api.nom_raison_sociale ?? api.siren,\n finances,\n dirigeants: (api.dirigeants ?? []).map(toDirigeant),\n etablissements,\n actif: (api.etat_administratif ?? \"A\") === \"A\",\n // Toujours présent côté retour DINUM pour cohérence du contrat caller :\n // distingue explicitement \"DINUM a répondu\" du fallback \"insee_v3\".\n siren_source: \"dinum\",\n ...pickDefined({\n siretSiege: api.siege?.siret,\n naf: api.activite_principale,\n nafLibelle: api.libelle_activite_principale,\n trancheEffectif: api.tranche_effectif_salarie,\n natureJuridique: api.nature_juridique,\n }),\n };\n if (api.nombre_etablissements !== undefined) {\n entreprise.nombreEtablissements = api.nombre_etablissements;\n }\n if (api.nombre_etablissements_ouverts !== undefined) {\n entreprise.nombreEtablissementsOuverts = api.nombre_etablissements_ouverts;\n }\n return entreprise;\n}\n\nfunction toDirigeant(api: ApiDirigeant): Dirigeant {\n return pickDefined({\n nom: api.nom,\n prenoms: api.prenoms,\n fonction: api.fonction,\n qualite: api.qualite,\n });\n}\n\nfunction toEtablissement(api: ApiSiege): Etablissement {\n const point = parseCoordinates(api.longitude, api.latitude);\n return {\n siret: api.siret ?? \"\",\n adresse: api.adresse ?? \"\",\n actif: (api.etat_administratif ?? \"A\") === \"A\",\n ...pickDefined({\n codePostal: api.code_postal,\n commune: api.libelle_commune,\n naf: api.activite_principale,\n trancheEffectif: api.tranche_effectif_salarie,\n dateCreation: api.date_creation,\n }),\n ...(point ? { point } : {}),\n };\n}\n","/**\n * Cache fichier local pour les dumps publics téléchargés (FINESS, Annuaire Ameli, INSEE…).\n *\n * Stratégie : un fichier de cache par dataset, refresh si plus vieux que `ttlMs`.\n * Pas de coordination multi-process — si deux processus tentent de refresh en\n * parallèle, le second écrase le premier (acceptable pour des dumps de référence\n * publics).\n *\n * Localisation par défaut : `~/.cache/france-data-mcp/` (suit XDG-ish sur macOS/Linux).\n */\n\nimport { existsSync } from \"node:fs\";\nimport { mkdir, rename, stat, unlink, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { DEFAULT_USER_AGENT } from \"./http.js\";\n\nexport type CacheOptions = {\n /** Dossier où stocker les caches (défaut : ~/.cache/france-data-mcp) */\n cacheDir?: string;\n /** Durée de vie en millisecondes avant refresh */\n ttlMs?: number;\n /** Forcer le refresh même si le cache est encore valide */\n force?: boolean;\n /** User-Agent à envoyer pour le téléchargement */\n userAgent?: string;\n signal?: AbortSignal;\n};\n\nexport const DEFAULT_CACHE_DIR = join(homedir(), \".cache\", \"france-data-mcp\");\n\n/**\n * Télécharge un fichier depuis une URL et le stocke en cache local.\n * Si le cache existe et a moins de `ttlMs`, on retourne le chemin sans re-télécharger.\n *\n * Renvoie le chemin local du fichier prêt à l'emploi.\n */\nexport async function downloadWithCache(\n url: string,\n cacheFileName: string,\n options: CacheOptions = {},\n): Promise<string> {\n const {\n cacheDir = DEFAULT_CACHE_DIR,\n ttlMs = 7 * 24 * 60 * 60 * 1000,\n force = false,\n userAgent = DEFAULT_USER_AGENT,\n signal,\n } = options;\n\n const cachePath = join(cacheDir, cacheFileName);\n\n if (!force && (await isCacheFresh(cachePath, ttlMs))) {\n return cachePath;\n }\n\n await mkdir(dirname(cachePath), { recursive: true });\n\n const response = await fetch(url, {\n headers: { \"User-Agent\": userAgent },\n signal,\n });\n if (!response.ok) {\n throw new Error(`Failed to download ${url}: HTTP ${response.status} ${response.statusText}`);\n }\n\n // Écriture atomique : on écrit dans un fichier .tmp, puis on `rename` qui est\n // atomique au niveau du FS. Si le download échoue ou que l'écriture est\n // interrompue, on n'a jamais un cachePath corrompu.\n const tmpPath = `${cachePath}.tmp.${process.pid}`;\n try {\n const buffer = Buffer.from(await response.arrayBuffer());\n await writeFile(tmpPath, buffer);\n await rename(tmpPath, cachePath);\n return cachePath;\n } catch (err) {\n console.error(\n `[france-data-mcp] cache write failed for ${url} → ${cachePath}: ${(err as Error).message}`,\n );\n await unlink(tmpPath).catch((unlinkErr: unknown) => {\n const code = (unlinkErr as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n console.warn(\n `[france-data-mcp] failed to clean up temp cache file ${tmpPath} (${code ?? \"unknown\"}): ${(unlinkErr as Error).message}`,\n );\n }\n });\n throw err;\n }\n}\n\nasync function isCacheFresh(filePath: string, ttlMs: number): Promise<boolean> {\n if (!existsSync(filePath)) return false;\n try {\n const stats = await stat(filePath);\n const ageMs = Date.now() - stats.mtimeMs;\n return ageMs < ttlMs;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n // ENOENT = race entre existsSync et stat, OK on retélécharge\n if (code === \"ENOENT\") return false;\n // EACCES, EROFS, ENOSPC = problème système réel, on doit le savoir\n console.error(\n `[france-data-mcp] cache stat failed unexpectedly for ${filePath} (${code ?? \"unknown\"}): ${(err as Error).message}`,\n );\n throw err;\n }\n}\n","/**\n * Parser CSV minimaliste pour les dumps publics français (séparateur `;`,\n * encoding UTF-8 avec BOM, quotes optionnels). Volontairement sans dépendance.\n *\n * Limites : ne gère pas les retours-ligne dans les valeurs quotées (rare dans\n * les dumps gouv.fr). Si un dataset le requiert, basculer vers une lib dédiée.\n */\n\n// U+FEFF, marqueur de début de fichier UTF-8 produit par Excel et certains\n// dumps data.gouv.fr. Caractère invisible — vérifier avec un éditeur qui\n// affiche les codes points si la ligne paraît vide.\nconst BOM = \"\";\n\nexport type CsvParseOptions = {\n /** Séparateur de champs (défaut `;`, standard FR) */\n delimiter?: string;\n /** Caractère de quotage (défaut `\"`) */\n quote?: string;\n};\n\n/**\n * Parse une ligne CSV en respectant les valeurs quotées.\n * Une valeur quotée peut contenir le délimiteur sans qu'il soit interprété.\n * Les guillemets doublés `\"\"` à l'intérieur d'une valeur quotée représentent un `\"`.\n */\nexport function parseCsvLine(line: string, options: CsvParseOptions = {}): string[] {\n const delimiter = options.delimiter ?? \";\";\n const quote = options.quote ?? '\"';\n const fields: string[] = [];\n let current = \"\";\n let inQuotes = false;\n let i = 0;\n\n while (i < line.length) {\n const char = line[i];\n\n if (inQuotes) {\n if (char === quote) {\n if (line[i + 1] === quote) {\n current += quote;\n i += 2;\n continue;\n }\n inQuotes = false;\n i++;\n continue;\n }\n current += char;\n i++;\n continue;\n }\n\n if (char === quote) {\n inQuotes = true;\n i++;\n continue;\n }\n if (char === delimiter) {\n fields.push(current);\n current = \"\";\n i++;\n continue;\n }\n current += char;\n i++;\n }\n\n fields.push(current);\n return fields;\n}\n\n/**\n * Parse un CSV complet en mémoire et retourne un tableau d'objets indexés par\n * les en-têtes de la première ligne. Strip le BOM UTF-8 si présent.\n */\nexport function parseCsv(\n content: string,\n options: CsvParseOptions = {},\n): Array<Record<string, string>> {\n const cleaned = content.startsWith(BOM) ? content.slice(BOM.length) : content;\n const lines = cleaned.split(/\\r?\\n/);\n const result: Array<Record<string, string>> = [];\n\n if (lines.length === 0) return result;\n const headers = parseCsvLine(lines[0] ?? \"\", options);\n\n for (let i = 1; i < lines.length; i++) {\n const line = lines[i];\n if (!line) continue;\n const values = parseCsvLine(line, options);\n const record: Record<string, string> = {};\n for (let j = 0; j < headers.length; j++) {\n const header = headers[j];\n if (header) record[header] = values[j] ?? \"\";\n }\n result.push(record);\n }\n\n return result;\n}\n\n/**\n * Stream un CSV ligne à ligne. Utile pour les fichiers volumineux que l'on ne\n * veut pas charger entièrement en mémoire (Annuaire Ameli ~146 Mo).\n *\n * Le générateur yield un objet par ligne de données. Le BOM et l'en-tête sont\n * gérés automatiquement.\n */\nexport async function* streamCsvLines(\n source: AsyncIterable<string>,\n options: CsvParseOptions = {},\n): AsyncGenerator<Record<string, string>> {\n let buffer = \"\";\n let headers: string[] | null = null;\n let firstChunk = true;\n\n for await (const chunk of source) {\n const data = firstChunk && chunk.startsWith(BOM) ? chunk.slice(BOM.length) : chunk;\n firstChunk = false;\n buffer += data;\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n if (!line) continue;\n if (!headers) {\n headers = parseCsvLine(line, options);\n continue;\n }\n yield rowToObject(headers, parseCsvLine(line, options));\n }\n }\n\n if (buffer.trim().length > 0 && headers) {\n yield rowToObject(headers, parseCsvLine(buffer, options));\n }\n}\n\nfunction rowToObject(headers: string[], values: string[]): Record<string, string> {\n const obj: Record<string, string> = {};\n for (let i = 0; i < headers.length; i++) {\n const header = headers[i];\n if (header) obj[header] = values[i] ?? \"\";\n }\n return obj;\n}\n\n/**\n * Adapte un Web ReadableStream<Uint8Array> en AsyncIterable<string> pour\n * `streamCsvLines`. Décode en UTF-8 strict (`fatal: true`) : si le dump\n * contient des octets invalides (latin-1 ou corruption), on throw au lieu de\n * silencieusement insérer des U+FFFD qui casseraient les filtres\n * insensibles à la casse sur les caractères accentués.\n */\nexport async function* streamReaderToStrings(\n reader: ReadableStream<Uint8Array>,\n): AsyncGenerator<string> {\n const decoder = new TextDecoder(\"utf-8\", { fatal: true });\n const r = reader.getReader();\n try {\n while (true) {\n const { done, value } = await r.read();\n if (done) {\n try {\n const final = decoder.decode();\n if (final) yield final;\n } catch (decodeErr) {\n console.error(\n `[france-data-mcp] UTF-8 decode error at end of stream: ${(decodeErr as Error).message}. Le dump n'est peut-être pas en UTF-8.`,\n );\n throw decodeErr;\n }\n break;\n }\n try {\n yield decoder.decode(value, { stream: true });\n } catch (decodeErr) {\n console.error(\n `[france-data-mcp] UTF-8 decode error in CSV stream: ${(decodeErr as Error).message}.`,\n );\n throw decodeErr;\n }\n }\n } finally {\n r.releaseLock();\n }\n}\n","/**\n * FINESS DREES category nomenclature — codes + libellés.\n *\n * Catalogue ~50 codes représentant ~92% du volume FINESS (95K rows total).\n * Le reliquat ~8% tombe en famille `autre` (codes très rares : thermal,\n * lieux de vie expérimentaux, structures atypiques).\n *\n * Source: live FINESS extract on data.gouv.fr. Re-verify against the CSV\n * when adding codes — DREES occasionally rotates labels (les libellés\n * sont copiés à l'identique du CSV pour matcher exactement la nomenclature\n * officielle).\n */\n\nexport const FINESS_CATEGORIES = {\n // ─── SANITAIRE — court séjour (MCO + adjacents) ───────────────────────\n \"101\": \"Centre Hospitalier Régional (C.H.R.)\",\n \"106\": \"Centre hospitalier\",\n \"108\": \"Centre Hospitalier Universitaire (C.H.U.)\",\n \"114\": \"Hôpital des armées\",\n \"115\": \"Etablissement de Soins du Service de Santé des Armées\",\n \"128\": \"Etablissement de Soins Chirurgicaux\",\n \"129\": \"Etablissement de Soins Médicaux\",\n \"131\": \"Centre de Lutte Contre le Cancer (C.L.C.C.)\",\n \"355\": \"Centre Hospitalier (C.H.)\",\n \"365\": \"Etablissement de Soins Pluridisciplinaire\",\n\n // ─── SANITAIRE — SSR / SLD / HAD / dialyse ────────────────────────────\n \"109\": \"Etablissement de santé privé autorisé en SSR\",\n \"362\": \"Etablissement de Soins Longue Durée (USLD)\",\n \"127\": \"Hospitalisation à Domicile (HAD)\",\n \"141\": \"Centre de dialyse\",\n \"146\": \"Structure d'Alternative à la dialyse en centre\",\n\n // ─── SANITAIRE — psychiatrie ──────────────────────────────────────────\n \"292\": \"Centre Hospitalier Spécialisé lutte Maladies Mentales\",\n \"156\": \"Centre Médico-Psychologique (C.M.P.)\",\n \"161\": \"Maison de Santé pour Maladies Mentales\",\n \"425\": \"Centre d'Accueil Thérapeutique à temps partiel (C.A.T.T.P.)\",\n \"430\": \"Centre Postcure Malades Mentaux\",\n\n // ─── AMBULATOIRE / soins de ville ─────────────────────────────────────\n \"124\": \"Centre de Santé\",\n \"603\": \"Maison de santé (L.6223-3)\",\n \"604\": \"Communautés professionnelles territoriales de santé (CPTS)\",\n\n // ─── PHARMACIE / BIO / IMAGERIE ───────────────────────────────────────\n \"611\": \"Laboratoire de Biologie Médicale\",\n \"619\": \"Cabinet d'imagerie médicale\",\n \"620\": \"Pharmacie d'Officine\",\n \"627\": \"Propharmacie\",\n\n // ─── PERSONNES ÂGÉES — hébergement ────────────────────────────────────\n \"500\": \"Etablissement d'hébergement pour personnes âgées dépendantes (EHPAD)\",\n \"501\": \"EHPA percevant des crédits d'assurance maladie\",\n \"502\": \"EHPA ne percevant pas des crédits d'assurance maladie\",\n \"202\": \"Résidences autonomie\",\n\n // ─── PERSONNES ÂGÉES — accompagnement ─────────────────────────────────\n \"207\": \"Centre de Jour pour Personnes Agées\",\n \"463\": \"Centres Locaux Information Coordination P.A. (C.L.I.C.)\",\n\n // ─── DOMICILE (médico-social + soins) ─────────────────────────────────\n \"354\": \"Service de Soins Infirmiers à Domicile (S.S.I.A.D.)\",\n \"460\": \"Service d'Aide et d'Accompagnement à Domicile (S.A.A.D.)\",\n \"209\": \"Service Polyvalent Aide et Soins à Domicile (S.P.A.S.A.D.)\",\n\n // ─── HANDICAP ENFANTS ─────────────────────────────────────────────────\n \"182\": \"Service d'Éducation Spéciale et de Soins à Domicile (SESSAD)\",\n \"183\": \"Institut Médico-Éducatif (I.M.E.)\",\n \"186\": \"Institut Thérapeutique Éducatif et Pédagogique (I.T.E.P.)\",\n \"188\": \"Etablissement pour Enfants ou Adolescents Polyhandicapés\",\n \"189\": \"Centre Médico-Psycho-Pédagogique (C.M.P.P.)\",\n \"190\": \"Centre Action Médico-Sociale Précoce (C.A.M.S.P.)\",\n \"192\": \"Institut d'éducation motrice\",\n \"194\": \"Institut pour Déficients Visuels\",\n \"195\": \"Institut pour Déficients Auditifs\",\n \"196\": \"Institut d'Education Sensorielle Sourd/Aveugle\",\n\n // ─── HANDICAP ADULTES ─────────────────────────────────────────────────\n \"246\": \"Etablissement et Service d'Aide par le Travail (E.S.A.T.)\",\n \"247\": \"Entreprise adaptée\",\n \"252\": \"Foyer Hébergement Adultes Handicapés\",\n \"255\": \"Maison d'Accueil Spécialisée (M.A.S.)\",\n \"382\": \"Foyer de Vie pour Adultes Handicapés\",\n \"437\": \"Foyer d'Accueil Médicalisé pour Adultes Handicapés (F.A.M.)\",\n \"445\": \"Service d'accompagnement médico-social adultes handicapés (SAMSAH)\",\n \"446\": \"Service d'Accompagnement à la Vie Sociale (S.A.V.S.)\",\n \"448\": \"Etab. Acc. Médicalisé en tout ou partie personnes handicapées\",\n \"449\": \"Etab. Accueil Non Médicalisé pour personnes handicapées\",\n \"600\": \"Foyer d'hébergement pour adultes handicapés\",\n\n // ─── ADDICTOLOGIE / accompagnement ────────────────────────────────────\n \"165\": \"Appartement de Coordination Thérapeutique (A.C.T.)\",\n \"178\": \"Centre Accueil/Accomp. Réduc. Risq. Usag. Drogues (C.A.A.R.U.D.)\",\n \"180\": \"Lits Halte Soins Santé (L.H.S.S.)\",\n \"197\": \"Centre soins accompagnement prévention addictologie (C.S.A.P.A.)\",\n \"412\": \"Appartement Thérapeutique\",\n\n // ─── ENFANCE / PROTECTION ─────────────────────────────────────────────\n \"175\": \"Foyer de l'Enfance\",\n \"177\": \"Maison d'Enfants à Caractère Social (MECS)\",\n \"295\": \"Services AEMO et AED\",\n \"236\": \"Centre Placement Familial Socio-Educatif (C.P.F.S.E.)\",\n \"238\": \"Centre d'Accueil Familial Spécialisé\",\n \"241\": \"Foyer d'Action Educative (F.A.E.)\",\n \"440\": \"Service Investigation Orientation Educative (S.I.O.E.)\",\n \"441\": \"Centre d'Action Educative (C.A.E.)\",\n \"378\": \"Etablissement Expérimental Enfance Protégée\",\n\n // ─── PMI / PETITE ENFANCE / SANTÉ SCOLAIRE ────────────────────────────\n \"223\": \"Protection Maternelle et Infantile (P.M.I.)\",\n \"228\": \"Centre Planification ou Education Familiale\",\n \"230\": \"Etablissement Consultation Protection Infantile\",\n \"268\": \"Centre Médico-Scolaire\",\n\n // ─── HÉBERGEMENT SOCIAL ───────────────────────────────────────────────\n \"214\": \"Centre Hébergement & Réinsertion Sociale (C.H.R.S.)\",\n \"219\": \"Autre Centre d'Accueil\",\n \"256\": \"Foyer Travailleurs Migrants non transformé en Résidence Sociale\",\n \"257\": \"Foyer de Jeunes Travailleurs (résidence sociale ou non)\",\n \"258\": \"Maisons Relais - Pensions de Famille\",\n \"259\": \"Autre Résidence Sociale (hors Maison Relais)\",\n \"443\": \"Centre Accueil Demandeurs Asile (C.A.D.A.)\",\n \"442\": \"Centre Provisoire Hébergement (C.P.H.)\",\n \"462\": \"Lieux de vie\",\n \"166\": \"Etablissement d'Accueil Mère-Enfant\",\n\n // ─── PRÉVENTION / SANTÉ PUBLIQUE ──────────────────────────────────────\n \"132\": \"Etablissement de Transfusion Sanguine\",\n \"142\": \"Dispensaire Antituberculeux\",\n \"143\": \"Centre de Vaccination BCG\",\n \"266\": \"Dispensaire Antivénérien\",\n \"347\": \"Centre d'Examens de Santé\",\n \"636\": \"Centre de soins et de prévention\",\n\n // ─── GROUPEMENTS ──────────────────────────────────────────────────────\n \"696\": \"Groupement de coopération sanitaire de moyens\",\n \"697\": \"Groupement de coopération sanitaire — Etablissement de santé\",\n\n // ─── HORS TAXONOMIE (voir DELIBERATELY_AUTRE pour la justification) ───\n \"126\": \"Etablissement Thermal\",\n \"632\": \"Structure Dispensatrice à domicile d'Oxygène à usage médical\",\n \"698\": \"Autre Etablissement Loi Hospitalière\",\n} as const satisfies Record<string, string>;\n\nexport type FinessCategorieCode = keyof typeof FINESS_CATEGORIES;\n\nexport function libelleCategorieFiness(code: string): string | undefined {\n return (FINESS_CATEGORIES as Record<string, string>)[code];\n}\n\n/**\n * FINESS family taxonomy. Drives the `familles` filter on the MCP tools.\n *\n * Each family is a precise, query-side tag — callers compose them via the\n * `familles` array. Designed for prospection commerciale santé : labos\n * ciblent MCO/EHPAD/CSI/MSP/CPTS/MAS/FAM/SLD/HAD, équipementiers ciblent\n * EHPAD/résidences autonomie, services à domicile ciblent SAAD/SPASAD/SSIAD…\n */\nexport type FinessFamille =\n // Sanitaire\n | \"mco\"\n | \"ssr\"\n | \"sld\"\n | \"had\"\n | \"psychiatrie\"\n | \"dialyse\"\n | \"ambulatoire\"\n // Bio / pharma / imagerie\n | \"labo\"\n | \"imagerie\"\n | \"pharmacie\"\n // Maisons + communautés professionnelles\n | \"msp_cpts\"\n // Personnes âgées\n | \"ehpad\"\n | \"residence_autonomie\"\n | \"senior_accompagnement\"\n // Domicile\n | \"ssiad\"\n | \"aide_domicile\"\n // Handicap\n | \"handicap_enfants\"\n | \"handicap_adultes\"\n // Addictologie + précarité sanitaire\n | \"addictologie\"\n // Enfance / protection / PMI\n | \"enfance_protection\"\n | \"pmi\"\n // Hébergement social\n | \"hebergement_social\"\n // Prévention / santé publique\n | \"prevention_sante\"\n // Groupements de coopération\n | \"groupement\"\n // Catch-all\n | \"autre\";\n\n/**\n * Family classification of FINESS DREES category codes.\n *\n * `autre` is the catch-all : codes in FINESS_CATEGORIES that don't fit any\n * specific family (ex. thermal). To get \"everything else\", omit the family\n * filter and post-filter via `result.categorie.famille`.\n *\n * The `query` subtype excludes \"autre\" — the MCP tools accept this set for\n * the `familles` parameter.\n */\nexport type FinessFamilleQuery = Exclude<FinessFamille, \"autre\">;\n\nexport const FINESS_FAMILY_CODES: Record<FinessFamilleQuery, readonly string[]> = {\n // Sanitaire — court séjour\n mco: [\"101\", \"106\", \"108\", \"114\", \"115\", \"128\", \"129\", \"131\", \"355\", \"365\"],\n ssr: [\"109\"],\n sld: [\"362\"],\n had: [\"127\"],\n psychiatrie: [\"292\", \"156\", \"161\", \"425\", \"430\"],\n dialyse: [\"141\", \"146\"],\n ambulatoire: [\"124\"],\n // Bio / pharma / imagerie\n labo: [\"611\"],\n imagerie: [\"619\"],\n pharmacie: [\"620\", \"627\"],\n // Pluri-pro\n msp_cpts: [\"603\", \"604\"],\n // Personnes âgées\n ehpad: [\"500\", \"501\", \"502\"],\n residence_autonomie: [\"202\"],\n senior_accompagnement: [\"207\", \"463\"],\n // Domicile\n ssiad: [\"354\"],\n aide_domicile: [\"460\", \"209\"],\n // Handicap\n handicap_enfants: [\"182\", \"183\", \"186\", \"188\", \"189\", \"190\", \"192\", \"194\", \"195\", \"196\"],\n handicap_adultes: [\"246\", \"247\", \"252\", \"255\", \"382\", \"437\", \"445\", \"446\", \"448\", \"449\", \"600\"],\n // Addictologie + précarité sanitaire\n addictologie: [\"165\", \"178\", \"180\", \"197\", \"412\"],\n // Enfance / protection\n enfance_protection: [\"175\", \"177\", \"236\", \"238\", \"241\", \"295\", \"378\", \"440\", \"441\"],\n // PMI / petite enfance\n pmi: [\"223\", \"228\", \"230\", \"268\"],\n // Hébergement social\n hebergement_social: [\"166\", \"214\", \"219\", \"256\", \"257\", \"258\", \"259\", \"442\", \"443\", \"462\"],\n // Prévention / santé publique\n prevention_sante: [\"132\", \"142\", \"143\", \"266\", \"347\", \"636\"],\n // Groupements\n groupement: [\"696\", \"697\"],\n} as const;\n\nconst FAMILY_BY_CODE: ReadonlyMap<string, FinessFamilleQuery> = new Map<string, FinessFamilleQuery>(\n (Object.keys(FINESS_FAMILY_CODES) as FinessFamilleQuery[]).flatMap((fam) =>\n FINESS_FAMILY_CODES[fam].map((code) => [code, fam] as const),\n ),\n);\n\n/**\n * Codes intentionally left in \"autre\" — checked by the invariant test so a\n * new FINESS_CATEGORIES entry without a family decision fails CI loudly.\n *\n * @internal Test-only export. Runtime code uses `finessFamille()`.\n */\nexport const DELIBERATELY_AUTRE = new Set<string>([\n \"126\", // Etablissement Thermal — pas de famille santé/médico-social pertinente\n \"632\", // Oxygénothérapie à domicile = PSAD (prestataire de santé à domicile,\n // dispositif médical), pas une aide à domicile au sens SAAD/SPASAD.\n // Volume marginal (<1%), pas de famille dédiée pour 1 code.\n \"698\", // \"Autre Etablissement Loi Hospitalière\" — fourre-tout DREES\n // hospitalier, pas un groupement (≠ GCS/GCSMS). Mieux vaut autre.\n]);\n\n/**\n * Classify a FINESS category code into a family for query-side filtering.\n *\n * Inputs are normalized first: `null`, `undefined`, empty string, and\n * whitespace-only strings all resolve to \"autre\". Non-empty inputs are trimmed\n * before matching, tolerating whitespace artefacts occasionally present in\n * DREES dumps (e.g. `\" 108 \"` → \"mco\").\n */\nexport function finessFamille(code: string | null | undefined): FinessFamille {\n const trimmed = code?.trim();\n if (!trimmed) return \"autre\";\n return FAMILY_BY_CODE.get(trimmed) ?? \"autre\";\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Stable convenience exports — used by lib consumers, kept for back-compat.\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * All hospital-grade categories (MCO acute-care + SSR + SLD + HAD + psy).\n * Use FINESS_FAMILY_CODES.mco for strict acute-care only.\n */\nexport const FINESS_HOPITAUX = [\n ...FINESS_FAMILY_CODES.mco,\n ...FINESS_FAMILY_CODES.ssr,\n ...FINESS_FAMILY_CODES.sld,\n ...FINESS_FAMILY_CODES.had,\n ...FINESS_FAMILY_CODES.psychiatrie,\n] as const;\n\nexport const FINESS_LABOS = FINESS_FAMILY_CODES.labo;\nexport const FINESS_PHARMACIES = FINESS_FAMILY_CODES.pharmacie;\nexport const FINESS_EHPAD = FINESS_FAMILY_CODES.ehpad;\nexport const FINESS_MSP_CPTS = FINESS_FAMILY_CODES.msp_cpts;\n","/**\n * FINESS — Fichier National des Établissements Sanitaires et Médico-Sociaux.\n *\n * Source : data.gouv.fr → dump CSV bimestriel ~35 Mo (`finess-extraction-du-fichier-des-etablissements`).\n * Variante géolocalisée : Atlasanté `referentiel-finess-t-finess` (~232 Mo).\n *\n * ⚠️ Migration ANS été 2026 : tous les datasets data.gouv portent l'avertissement\n * que la génération du flux actuel s'arrêtera. Surveiller le repo\n * github.com/ansforge/finess pour le nouveau format (probablement FHIR-compatible).\n *\n * Cette fonction télécharge le CSV avec cache 7j puis charge en mémoire (~35 Mo\n * → ~70 Mo de RAM résidente Node). Adapté à un usage CLI ou serveur node long.\n * Pour un usage serverless (Vercel Edge), ne pas charger l'intégralité —\n * préférer une DB externe (PostGIS, DuckDB).\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { type CacheOptions, downloadWithCache } from \"../core/cache.js\";\nimport { parseCoordinates } from \"../core/coords.js\";\nimport { parseCsv } from \"../core/csv.js\";\nimport { pickDefined } from \"../core/object-utils.js\";\nimport type { Coordinates } from \"../core/types.js\";\nimport { libelleCategorieFiness } from \"./finess-categories.js\";\n\nconst FINESS_CSV_URL =\n \"https://www.data.gouv.fr/api/1/datasets/r/3dc9b1d5-0157-440d-a7b5-c894fcfdfd45\";\nconst FINESS_CACHE_FILE = \"finess-etablissements.csv\";\n\nexport type EtablissementFiness = {\n /** Numéro FINESS de l'entité géographique (ET) sur 9 chiffres */\n finessEt: string;\n /** Numéro FINESS de l'entité juridique (EJ) à laquelle l'ET est rattaché */\n finessEj?: string;\n /** Raison sociale (longue, plus complète que le nom court) */\n raisonSociale: string;\n /** Code de catégorie d'établissement (ex: \"500\" pour EHPAD) */\n categorieCode?: string;\n /** Libellé de la catégorie (mappé depuis FINESS_CATEGORIES si reconnu) */\n categorieLibelle?: string;\n /** Adresse ligne complète */\n adresse?: string;\n /** Code postal */\n codePostal?: string;\n /** Commune */\n commune?: string;\n /** Code INSEE de la commune (5 caractères) */\n codeCommune?: string;\n /** Code département (2 ou 3 caractères) */\n departement?: string;\n /** Coordonnées GPS (présentes dans le dump géolocalisé Atlasanté) */\n point?: Coordinates;\n /** Téléphone */\n telephone?: string;\n /** SIREN si renseigné */\n siren?: string;\n};\n\nexport type LoadFinessOptions = CacheOptions & {\n /**\n * Chemin local d'un CSV déjà téléchargé (court-circuite le download).\n *\n * @security Cette option fait un `readFile` direct du chemin fourni. Ne\n * JAMAIS la forwarder depuis une entrée non-trustée (requête HTTP, args MCP) :\n * c'est un read fichier local non restreint. Strictement réservé à un usage\n * Node.js trusted.\n */\n csvPath?: string;\n};\n\nexport type SearchFinessOptions = {\n /** Filtre par codes de catégorie (ex: [\"500\"] pour EHPAD seuls) */\n categories?: string[];\n /** Filtre par code postal exact */\n codePostal?: string;\n /** Filtre par code département */\n departement?: string;\n /** Filtre par code commune INSEE */\n codeCommune?: string;\n /** Recherche géographique : centre + rayon en km (nécessite dump géolocalisé) */\n center?: Coordinates;\n /** Rayon en km */\n radiusKm?: number;\n /** Limite de résultats (défaut tous) */\n limit?: number;\n};\n\n/**\n * Charge l'index FINESS en mémoire. Télécharge le CSV si pas en cache.\n * Le résultat est utilisable plusieurs fois sans re-charger.\n */\nexport async function loadFiness(options: LoadFinessOptions = {}): Promise<EtablissementFiness[]> {\n const csvPath =\n options.csvPath ?? (await downloadWithCache(FINESS_CSV_URL, FINESS_CACHE_FILE, options));\n\n const content = await readFile(csvPath, \"utf-8\");\n const rows = parseCsv(content, { delimiter: \";\" });\n\n // Une seule passe pour mapper + filtrer les lignes invalides : évite\n // d'allouer un tableau intermédiaire de ~120k éléments pour FINESS.\n const ets: EtablissementFiness[] = [];\n for (const row of rows) {\n const e = toEtablissementFiness(row);\n if (e !== null) ets.push(e);\n }\n\n // Si plus de 5% des lignes sont droppées (champ FINESS_ET manquant), c'est\n // probablement une migration de schéma upstream (annoncée par l'ANS pour\n // l'été 2026). Le silence serait dangereux : `searchEtablissementsFiness`\n // renverrait `[]` sans alerte alors que l'index est cassé.\n const total = rows.length;\n if (total > 100) {\n const dropRate = (total - ets.length) / total;\n if (dropRate > 0.05) {\n console.error(\n `[france-data-mcp] FINESS: ${total - ets.length}/${total} lignes invalides (${(dropRate * 100).toFixed(1)}%). Schéma CSV probablement changé. Colonnes attendues: nofinesset, rs, categetab, cpostal, commune. Migration ANS prévue été 2026 — vérifier github.com/ansforge/finess et https://www.data.gouv.fr/datasets/finess-extraction-du-fichier-des-etablissements-sanitaires-et-sociaux/`,\n );\n }\n }\n\n return ets;\n}\n\n/**\n * Recherche des établissements dans un index FINESS pré-chargé.\n * Pour avoir l'index : `const index = await loadFiness();`\n */\nexport function searchEtablissementsFiness(\n index: EtablissementFiness[],\n options: SearchFinessOptions,\n): EtablissementFiness[] {\n const { categories, codePostal, departement, codeCommune, center, radiusKm, limit } = options;\n\n if (center && (radiusKm === undefined || radiusKm <= 0)) {\n throw new Error(\"searchEtablissementsFiness: radiusKm > 0 requis quand center est fourni\");\n }\n\n const radiusMeters = center && radiusKm !== undefined ? radiusKm * 1000 : null;\n const categoriesSet = categories ? new Set(categories) : null;\n\n const matches: EtablissementFiness[] = [];\n for (const e of index) {\n if (categoriesSet && (!e.categorieCode || !categoriesSet.has(e.categorieCode))) continue;\n if (codePostal && e.codePostal !== codePostal) continue;\n if (departement && e.departement !== departement) continue;\n if (codeCommune && e.codeCommune !== codeCommune) continue;\n if (center && radiusMeters !== null) {\n if (!e.point) continue;\n if (haversineDistance(center, e.point) > radiusMeters) continue;\n }\n matches.push(e);\n if (limit !== undefined && matches.length >= limit) break;\n }\n\n return matches;\n}\n\n/**\n * Distance Haversine entre deux points GPS, en mètres.\n * Formule sphérique standard (suffisante pour les rayons < 100 km).\n */\nexport function haversineDistance(a: Coordinates, b: Coordinates): number {\n const R = 6_371_000;\n const lat1 = toRad(a.lat);\n const lat2 = toRad(b.lat);\n const deltaLat = toRad(b.lat - a.lat);\n const deltaLon = toRad(b.lon - a.lon);\n const h =\n Math.sin(deltaLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLon / 2) ** 2;\n return 2 * R * Math.asin(Math.sqrt(h));\n}\n\nfunction toRad(deg: number): number {\n return (deg * Math.PI) / 180;\n}\n\n/**\n * Mapping des colonnes du CSV FINESS vers le type interne.\n * Le CSV data.gouv.fr utilise les en-têtes suivants (varie légèrement selon\n * l'export, on est défensif sur les noms alternatifs).\n */\nfunction toEtablissementFiness(row: Record<string, string>): EtablissementFiness | null {\n const finessEt = row.nofinesset ?? row[\"FINESS ET\"] ?? row.finesset;\n if (!finessEt) return null;\n\n const categorieCode = row.categetab ?? row.categagretab ?? row.libcategetab;\n const categorieLibelle = categorieCode\n ? (libelleCategorieFiness(categorieCode) ?? row.libcategetab)\n : undefined;\n\n const adresseParts = [row.numvoie, row.typvoie, row.voie, row.compvoie].filter(Boolean);\n const adresse = adresseParts.length > 0 ? adresseParts.join(\" \").trim() : row.adresse;\n\n const point = parseCoordinates(row.coordxet ?? row.longitude, row.coordyet ?? row.latitude);\n\n return {\n finessEt,\n raisonSociale: row.rs ?? row.raisonsociale ?? row[\"Raison sociale\"] ?? row.rslongue ?? \"\",\n ...pickDefined({\n finessEj: row.nofinessej ?? row[\"FINESS EJ\"] ?? row.finessej,\n categorieCode,\n categorieLibelle,\n adresse,\n codePostal: row.cpostal ?? row.codepostal,\n commune: row.commune ?? row.libcommune,\n codeCommune: row.codecommune ?? row.codinsee,\n departement: row.departement ?? row.codedepartement,\n telephone: row.telephone ?? row.tel,\n siren: row.siren,\n }),\n ...(point ? { point } : {}),\n };\n}\n","/**\n * Annuaire Santé Ameli — répertoire des professionnels de santé libéraux conventionnés.\n *\n * Source : data.gouv.fr `annuaire-sante-ameli` (CSV ~146 Mo, ~1,5M lignes).\n * MAJ hebdomadaire (régénération chaque dimanche/lundi).\n *\n * ⚠️ Article L.1461-2 CSP : ces données contiennent des informations à\n * caractère personnel. Leur réutilisation est soumise au respect de la\n * réglementation relative à la protection de la vie privée. Toute application\n * publique doit afficher la mention \"Source : Annuaire santé Ameli, Assurance\n * Maladie\" et la date de la dernière sync.\n *\n * Volume : 146 Mo → trop pour charger en mémoire intégralement. Cette lib\n * propose un parser **streaming** : on lit le CSV ligne par ligne et on filtre\n * à la volée. Pour faire de la recherche par rayon géographique, il faut au\n * préalable géocoder les adresses (à faire côté caller, pas géré ici).\n */\n\nimport { createReadStream } from \"node:fs\";\nimport { type CacheOptions, downloadWithCache } from \"../core/cache.js\";\nimport { streamCsvLines } from \"../core/csv.js\";\nimport { pickDefined } from \"../core/object-utils.js\";\n\nconst ANNUAIRE_AMELI_CSV_URL =\n \"https://www.data.gouv.fr/api/1/datasets/r/3a700a1c-3079-4f7f-9bd7-83611e3f5e35\";\nconst ANNUAIRE_AMELI_CACHE_FILE = \"annuaire-sante-ameli-ps.csv\";\n\nexport type ProfessionnelSante = {\n /** Nom de famille (en exercice) */\n nom: string;\n /** Prénom(s) (en exercice) */\n prenom: string;\n /** Civilité (Dr, Mme, M.…) */\n civilite?: string;\n /** Raison sociale du lieu d'exercice si applicable */\n raisonSociale?: string;\n /** Code spécialité Ameli */\n specialiteCode?: string;\n /** Libellé de la spécialité (ex: \"Médecin généraliste\", \"Cardiologue\") */\n specialiteLibelle?: string;\n /** Code type de PS (médecin, IDE, sage-femme, pharmacien…) */\n typePsCode?: string;\n /** Libellé du type de PS */\n typePsLibelle?: string;\n /** Voie + numéro */\n adresse?: string;\n /** Complément d'adresse (étage, bâtiment…) */\n complementAdresse?: string;\n /** Code postal du lieu d'exercice */\n codePostal?: string;\n /** Commune du lieu d'exercice */\n commune?: string;\n /** Téléphone */\n telephone?: string;\n /** Secteur conventionnel (1, 2, 3, NC…) */\n secteurConventionnel?: string;\n /** Libellé du secteur conventionnel */\n secteurConventionnelLibelle?: string;\n /** Mode d'exercice (libéral, salarié, mixte…) */\n natureExercice?: string;\n};\n\nexport type StreamAnnuaireOptions = CacheOptions & {\n /**\n * Chemin local d'un CSV déjà téléchargé (court-circuite le download).\n *\n * @security Cette option ouvre un `createReadStream` direct sur le chemin\n * fourni. Ne JAMAIS la forwarder depuis une entrée non-trustée (requête\n * HTTP, args MCP) : c'est une lecture fichier local non restreinte qui peut\n * exposer des fichiers sensibles. Strictement réservé à un usage Node.js\n * trusted (CLI, script, code applicatif).\n */\n csvPath?: string;\n};\n\nexport type FilterAnnuaireOptions = {\n /** Filtre exact par code postal */\n codePostal?: string;\n /** Filtre par préfixe de code postal (ex: \"08\" pour tout le département 08) */\n codePostalPrefix?: string;\n /** Filtre par nom de commune (insensible à la casse) */\n commune?: string;\n /** Filtre par libellé de spécialité (insensible à la casse, contient) */\n specialite?: string;\n /** Filtre par code spécialité exact */\n specialiteCode?: string;\n /** Filtre par type de PS (Médecin, IDE, etc., insensible à la casse) */\n typePs?: string;\n /** Filtre par secteur conventionnel (\"1\", \"2\"…) */\n secteurConventionnel?: string;\n /** Limite (arrête le stream une fois atteinte) */\n limit?: number;\n};\n\n/**\n * S'assure que le CSV Annuaire Ameli est en cache local et renvoie son chemin.\n * Télécharge si nécessaire (~146 Mo, ~30 secondes en bonne connexion).\n */\nexport async function ensureAnnuaireAmeli(options: StreamAnnuaireOptions = {}): Promise<string> {\n if (options.csvPath) return options.csvPath;\n return downloadWithCache(ANNUAIRE_AMELI_CSV_URL, ANNUAIRE_AMELI_CACHE_FILE, options);\n}\n\n/**\n * Stream les professionnels de santé un par un, avec filtres optionnels.\n * Utilise un parser CSV streaming pour ne pas charger les 146 Mo en mémoire.\n *\n * @example Tous les MG du 08\n * ```ts\n * const out: string[] = [];\n * for await (const ps of streamProfessionnels({ codePostalPrefix: \"08\", specialite: \"généraliste\" })) {\n * out.push(`${ps.nom} ${ps.prenom} - ${ps.commune}`);\n * }\n * ```\n */\nexport async function* streamProfessionnels(\n options: StreamAnnuaireOptions & FilterAnnuaireOptions = {},\n): AsyncGenerator<ProfessionnelSante> {\n const csvPath = await ensureAnnuaireAmeli(options);\n\n const fileStream = createReadStream(csvPath, { encoding: \"utf-8\" });\n\n const {\n codePostal,\n codePostalPrefix,\n commune,\n specialite,\n specialiteCode,\n typePs,\n secteurConventionnel,\n limit,\n } = options;\n\n const specialiteLower = specialite?.toLowerCase();\n const communeLower = commune?.toLowerCase();\n const typePsLower = typePs?.toLowerCase();\n let yielded = 0;\n let parsed = 0;\n let skipped = 0;\n\n try {\n const stringStream = nodeReadableToAsyncIterable(fileStream);\n for await (const row of streamCsvLines(stringStream, { delimiter: \";\" })) {\n parsed++;\n const ps = toProfessionnelSante(row);\n if (!ps) {\n skipped++;\n continue;\n }\n\n if (codePostal && ps.codePostal !== codePostal) continue;\n if (codePostalPrefix && (!ps.codePostal || !ps.codePostal.startsWith(codePostalPrefix)))\n continue;\n if (communeLower && (!ps.commune || !ps.commune.toLowerCase().includes(communeLower)))\n continue;\n if (\n specialiteLower &&\n (!ps.specialiteLibelle || !ps.specialiteLibelle.toLowerCase().includes(specialiteLower))\n )\n continue;\n if (specialiteCode && ps.specialiteCode !== specialiteCode) continue;\n if (\n typePsLower &&\n (!ps.typePsLibelle || !ps.typePsLibelle.toLowerCase().includes(typePsLower))\n )\n continue;\n if (secteurConventionnel && ps.secteurConventionnel !== secteurConventionnel) continue;\n\n yield ps;\n yielded++;\n if (limit !== undefined && yielded >= limit) return;\n }\n } finally {\n // Détruit explicitement le stream pour libérer le file descriptor même si\n // le caller fait `break` ou `return` au milieu (cas typique avec `limit`).\n fileStream.destroy();\n // Si plus de 10% des lignes parsées sont invalides, c'est probablement un\n // changement de schéma upstream (Ameli renomme une colonne, format CSV\n // qui évolue). Le skip silencieux serait dangereux : un caller pourrait\n // recevoir 0 résultat alors que le fichier en contient des milliers.\n if (parsed > 100 && skipped > parsed * 0.1) {\n console.warn(\n `[france-data-mcp] annuaire-ameli: ${skipped}/${parsed} lignes invalides (${((skipped / parsed) * 100).toFixed(1)}%). Schéma CSV peut-être changé. Colonnes attendues: ps_activite_nom, ps_activite_prenom, specialite_libelle, coordonnees_code_postal, coordonnees_ville. Vérifier https://www.data.gouv.fr/datasets/annuaire-sante-ameli/`,\n );\n }\n }\n}\n\n/**\n * Charge un sous-ensemble filtré en mémoire (pratique pour les zones géographiques\n * étroites — un département entier reste raisonnable).\n */\nexport async function loadProfessionnels(\n options: StreamAnnuaireOptions & FilterAnnuaireOptions = {},\n): Promise<ProfessionnelSante[]> {\n const result: ProfessionnelSante[] = [];\n for await (const ps of streamProfessionnels(options)) {\n result.push(ps);\n }\n return result;\n}\n\nasync function* nodeReadableToAsyncIterable(\n readable: NodeJS.ReadableStream,\n): AsyncGenerator<string> {\n try {\n for await (const chunk of readable) {\n yield typeof chunk === \"string\" ? chunk : chunk.toString(\"utf-8\");\n }\n } finally {\n if (\"destroy\" in readable && typeof readable.destroy === \"function\") {\n readable.destroy();\n }\n }\n}\n\nfunction toProfessionnelSante(row: Record<string, string>): ProfessionnelSante | null {\n const nom = row.ps_activite_nom ?? \"\";\n const prenom = row.ps_activite_prenom ?? \"\";\n if (!nom && !prenom) return null;\n\n return {\n nom,\n prenom,\n ...pickDefined({\n civilite: row.ps_activite_civilite,\n raisonSociale: row.ps_activite_raison_sociale,\n specialiteCode: row.specialite_code,\n specialiteLibelle: row.specialite_libelle,\n typePsCode: row.type_ps_code,\n typePsLibelle: row.type_ps_libelle,\n adresse: row.coordonnees_voie,\n complementAdresse: row.coordonnees_complement || row.coordonnees_lieu_dit,\n codePostal: row.coordonnees_code_postal,\n commune: row.coordonnees_ville,\n telephone: row.coordonnees_num_tel,\n secteurConventionnel: row.secteur_conventionnel_code,\n secteurConventionnelLibelle: row.secteur_conventionnel_libelle,\n natureExercice: row.nature_exercice_libelle || row.nature_exercice_code,\n }),\n };\n}\n","/**\n * Mapping des codes NAF utiles pour l'analyse santé / médico-social.\n *\n * Source : nomenclature NAF rév.2 INSEE (2008).\n * Liste non exhaustive — focus sur les activités santé pertinentes pour\n * l'intelligence territoriale (implantation, prospection, audit).\n */\n\nexport const NAF_SANTE = {\n // Hôpitaux et cliniques\n \"8610Z\": \"Activités hospitalières\",\n\n // Pratique médicale et dentaire\n \"8621Z\": \"Activités de médecine générale\",\n \"8622A\": \"Activités de radiodiagnostic et de radiothérapie\",\n \"8622B\": \"Activités chirurgicales\",\n \"8622C\": \"Autres activités des médecins spécialistes\",\n \"8623Z\": \"Pratique dentaire\",\n\n // Autres activités de santé\n \"8690A\": \"Ambulances\",\n \"8690B\": \"Laboratoires d'analyses médicales\",\n \"8690C\": \"Centres de collecte et banques d'organes\",\n \"8690D\": \"Activités des infirmiers et des sages-femmes\",\n \"8690E\":\n \"Activités des professionnels de la rééducation, de l'appareillage et des pédicures-podologues\",\n \"8690F\": \"Activités de santé humaine non classées ailleurs\",\n\n // Hébergement médico-social\n \"8710A\": \"Hébergement médicalisé pour personnes âgées\",\n \"8710B\": \"Hébergement médicalisé pour enfants handicapés\",\n \"8710C\": \"Hébergement médicalisé pour adultes handicapés et autre hébergement médicalisé\",\n \"8720A\": \"Hébergement social pour handicapés mentaux et malades mentaux\",\n \"8720B\": \"Hébergement social pour toxicomanes\",\n \"8730A\": \"Hébergement social pour personnes âgées\",\n \"8730B\": \"Hébergement social pour handicapés physiques\",\n\n // Action sociale sans hébergement\n \"8810A\": \"Aide à domicile\",\n \"8810B\": \"Accueil ou accompagnement sans hébergement d'adultes handicapés ou de personnes âgées\",\n \"8810C\": \"Aide par le travail\",\n \"8891A\": \"Accueil de jeunes enfants\",\n \"8891B\": \"Accueil ou accompagnement sans hébergement d'enfants handicapés\",\n \"8899A\": \"Autre accueil ou accompagnement sans hébergement d'enfants et d'adolescents\",\n \"8899B\": \"Action sociale sans hébergement n.c.a.\",\n\n // Pharmacies et commerce de matériel médical\n \"4773Z\": \"Commerce de détail de produits pharmaceutiques en magasin spécialisé\",\n \"4774Z\": \"Commerce de détail d'articles médicaux et orthopédiques en magasin spécialisé\",\n} as const satisfies Record<string, string>;\n\nexport type NafCodeSante = keyof typeof NAF_SANTE;\n\n/**\n * Codes NAF correspondant aux laboratoires de biologie médicale et activités proches.\n */\nexport const NAF_LABOS = [\"8690B\"] as const satisfies readonly NafCodeSante[];\n\n/**\n * Codes NAF correspondant aux pharmacies d'officine.\n */\nexport const NAF_PHARMACIES = [\"4773Z\"] as const satisfies readonly NafCodeSante[];\n\n/**\n * Codes NAF correspondant aux EHPAD et hébergement médicalisé pour personnes âgées.\n */\nexport const NAF_EHPAD = [\"8710A\", \"8730A\"] as const satisfies readonly NafCodeSante[];\n\n/**\n * Codes NAF correspondant à la médecine de ville (généralistes + spécialistes).\n */\nexport const NAF_MEDECINE_VILLE = [\n \"8621Z\",\n \"8622A\",\n \"8622B\",\n \"8622C\",\n] as const satisfies readonly NafCodeSante[];\n\n/**\n * Renvoie le libellé d'un code NAF santé, ou undefined si non répertorié.\n */\nexport function libelleNaf(code: string): string | undefined {\n return (NAF_SANTE as Record<string, string>)[code];\n}\n","/**\n * Métadonnées de requête exposées dans les réponses des tools de listing.\n *\n * Pourquoi : le caller MCP (Claude.ai, Cursor, agent LLM) ne peut pas deviner\n * la nature du calcul de distance ni la précision géographique en lisant un\n * `distance_km: 2.67`. Or les sources varient : FINESS expose des coords\n * Lambert93 reprojetées en WGS84 (précision adresse), Ameli ne fournit que\n * le centroïde commune (~3 km moyenne). Sans cette transparence, un caller\n * peut prendre des décisions logistiques fausses (ex: \"le LBM est à 2.67 km\n * vol d'oiseau\" — mais la distance routière fait facilement +20-30%).\n *\n * Pattern aligné sur le bloc `fallback` déjà présent dans\n * `entreprises_in_radius` (cf. `api/tools.ts`) qui surface honnêtement la\n * stratégie de fallback API DINUM.\n */\n\n/**\n * Précision géographique des coordonnées exposées dans les résultats.\n *\n * - `lambert93_natif_finess` : coords FINESS DREES (Lambert 93 reprojeté\n * WGS84 à l'ingestion). Précision adresse ~10 m côté DREES.\n * - `centroide_commune_ameli` : coords Ameli (centroïde commune via\n * `geo.api.gouv.fr/communes`). Précision ~3 km moyenne — adapté à\n * l'analyse de densité, PAS au géocodage adresse.\n */\nexport type GeoPrecision =\n | \"lambert93_natif_finess\"\n | \"centroide_commune_ameli\"\n | \"centroide_commune_ans\"\n | \"structure_finess\";\n\n/**\n * Méthode de calcul des distances exposées dans `distance_km`.\n *\n * - `haversine_postgis` : ST_Distance sur le type `geography` PostGIS.\n * Distance vol d'oiseau, pas routière. Pour la distance routière,\n * intégrer un service externe (OSRM, ORS) côté caller.\n */\nexport type DistanceType = \"haversine_postgis\";\n\nexport interface QueryMetadata {\n geo_precision: GeoPrecision;\n /** Présent uniquement quand la requête expose `distance_km` (radius). */\n distance_type?: DistanceType;\n /** Notes actionnables pour le caller (précision attendue, cross-checks…). */\n notes: string[];\n}\n\nconst SOURCE_NOTE: Record<GeoPrecision, string> = {\n centroide_commune_ameli:\n \"Coordonnées Ameli = centroïde commune (~3 km moyenne). Adapté à l'analyse de densité médicale, pas au géocodage adresse.\",\n lambert93_natif_finess:\n \"FINESS DREES (sync bimestrielle) — référentiel peut avoir 1-2 mois de retard sur le terrain pour les structures émergentes (CPTS récentes, MSP en agrément). Cross-check ARS / Service Public si nécessaire.\",\n centroide_commune_ans:\n \"Coordonnées RPPS/ANS = centroïde commune (~3 km moyenne). Source : Annuaire Santé ANS — Licence Ouverte v2.0. Pour une précision adresse, croiser num_finess avec etablissement_by_finess.\",\n structure_finess:\n \"Liste rattachée à un FINESS site. Le mode_exercice révèle la nature du lien (libéral / salarié). Couverture RPPS quand le PS l'a déclaré ; salariés CH/CHU/cliniques bien couverts.\",\n};\n\nconst HAVERSINE_NOTE =\n \"Distance calculée en vol d'oiseau (haversine PostGIS). Pour la distance routière, croiser avec un service externe (OSRM, ORS).\";\n\n/**\n * Builder unique pour les 4 cas (Ameli/FINESS × radius/list). Factorise les\n * 4 helpers historiques. Si une nouvelle source spatiale arrive (IRIS, RPPS),\n * ajouter une entrée à `SOURCE_NOTE` et un alias à la fin du fichier.\n */\nfunction buildMetadata(precision: GeoPrecision, withDistance: boolean): QueryMetadata {\n const notes = [SOURCE_NOTE[precision]];\n const result: QueryMetadata = { geo_precision: precision, notes };\n if (withDistance) {\n result.distance_type = \"haversine_postgis\";\n notes.push(HAVERSINE_NOTE);\n }\n return result;\n}\n\nexport const ameliRadiusMetadata = (): QueryMetadata =>\n buildMetadata(\"centroide_commune_ameli\", true);\n\nexport const ameliDeptMetadata = (): QueryMetadata =>\n buildMetadata(\"centroide_commune_ameli\", false);\n\nexport const finessRadiusMetadata = (): QueryMetadata =>\n buildMetadata(\"lambert93_natif_finess\", true);\n\nexport const finessByCategorieMetadata = (): QueryMetadata =>\n buildMetadata(\"lambert93_natif_finess\", false);\n\nexport const rppsRadiusMetadata = (): QueryMetadata => buildMetadata(\"centroide_commune_ans\", true);\n\nexport const rppsDeptMetadata = (): QueryMetadata => buildMetadata(\"centroide_commune_ans\", false);\n\nexport const rppsEtablissementMetadata = (): QueryMetadata =>\n buildMetadata(\"structure_finess\", false);\n\n/**\n * Métadonnées pour `rpps_search_by_name` : recherche fuzzy par identité. La\n * géo précision reste celle d'ANS (centroïde commune) ; l'ajout sémantique est\n * la note de scoring trigram qui prévient le caller que les résultats sont\n * triés par pertinence et non par exactitude, et qu'un `match_score < 0.5`\n * indique souvent une homonymie partielle.\n */\nexport const rppsSearchByNameMetadata = (): QueryMetadata => {\n const md = buildMetadata(\"centroide_commune_ans\", false);\n md.notes.push(\n \"Résultats triés par similarité trigram (pg_trgm) sur nom + prénom. Le champ `match_score` (0..1) indique la pertinence — un score < 0.5 = homonymie partielle, à confirmer côté caller.\",\n );\n return md;\n};\n","import { type SupabaseClient, createClient } from \"@supabase/supabase-js\";\nimport type { Database } from \"./supabase-types.js\";\n\nlet anonClient: SupabaseClient<Database> | null = null;\nlet serviceClient: SupabaseClient<Database> | null = null;\nlet untypedAnonClient: SupabaseClient | null = null;\n\n/**\n * Read a required environment variable, distinguishing \"absent\" from \"set\n * but empty\" — the latter is the typical signature of a misconfigured GitHub\n * Secret (renamed, unscoped, or out-of-org). Exported so ingestion scripts\n * (`scripts/ingest/*`) can reuse the same diagnostic.\n */\nexport function requireEnv(name: string): string {\n const value = process.env[name];\n if (value === undefined) {\n throw new Error(\n `[france-data-mcp] Missing required environment variable: ${name}. Set it in .env.local for local dev or in GitHub Secrets for CI/Actions.`,\n );\n }\n // GitHub Actions substitutes \"\" for `${{ secrets.X }}` when X is renamed,\n // unset, or out-of-scope. Distinguishing this from \"var truly missing\" gives\n // operators a faster diagnosis path than a generic \"missing var\" message.\n if (value === \"\") {\n throw new Error(\n `[france-data-mcp] Environment variable ${name} is set but empty. Likely a misconfigured GitHub Secret (renamed/unscoped) or an empty line in .env.local.`,\n );\n }\n return value;\n}\n\n/**\n * Read-only client used by MCP tools. Respects RLS policies (only SELECT on\n * tables that grant anon read access).\n */\nexport function getAnonClient(): SupabaseClient<Database> {\n if (!anonClient) {\n const url = requireEnv(\"SUPABASE_URL\");\n const key = requireEnv(\"SUPABASE_ANON_KEY\");\n anonClient = createClient<Database>(url, key, {\n auth: { persistSession: false },\n });\n }\n return anonClient;\n}\n\n/**\n * Privileged client used ONLY by ingestion scripts running in GitHub Actions.\n * Bypasses RLS — never expose to end users.\n */\nexport function getServiceClient(): SupabaseClient<Database> {\n if (!serviceClient) {\n const url = requireEnv(\"SUPABASE_URL\");\n const key = requireEnv(\"SUPABASE_SERVICE_ROLE_KEY\");\n serviceClient = createClient<Database>(url, key, {\n auth: { persistSession: false },\n });\n }\n return serviceClient;\n}\n\n/**\n * Read-only client SANS typage Database — utilisé pour les RPCs ajoutées par\n * une migration qui n'a pas encore été suivie d'un `pnpm db:types` (donc\n * absentes du type généré). Bypass purement TypeScript : runtime identique au\n * client typé. Caller responsable du typage des params + du retour.\n *\n * Pattern miroir de `getUntypedServiceClient` côté `scripts/ingest/shared.ts`\n * pour les staging tables. À utiliser temporairement le temps qu'une regen\n * de types soit faite post-merge.\n */\nexport function getUntypedAnonClient(): SupabaseClient {\n if (!untypedAnonClient) {\n const url = requireEnv(\"SUPABASE_URL\");\n const key = requireEnv(\"SUPABASE_ANON_KEY\");\n untypedAnonClient = createClient(url, key, { auth: { persistSession: false } });\n }\n return untypedAnonClient;\n}\n\n/** Test-only helper: forces clients to be re-created on next call. */\nexport function __resetClientsForTesting(): void {\n anonClient = null;\n serviceClient = null;\n untypedAnonClient = null;\n}\n","/**\n * Helpers shared between the typed DB wrappers (`finess-db.ts`, `ameli-db.ts`,\n * future `iris-db.ts`) — extracted in V0.4 to remove duplication that was\n * about to ramify across each new ingester.\n *\n * Conventions:\n * - Pure functions, no side effects beyond throwing on bad input.\n * - `[france-data-mcp]` log prefix already in place upstream — these\n * helpers just throw, callers preserve their domain prefix.\n */\n\nexport const DEFAULT_LIMIT = 100;\nexport const MAX_LIMIT = 500;\n/**\n * Plafond OFFSET : 100K. Au-delà, Postgres scan une part énorme de la table\n * pour atteindre le row N — donc soit le caller paginate à un volume qu'il\n * ne devrait pas (re-design : filtrer plus en amont), soit c'est une faute\n * de saisie (5 zéros au lieu de 4). Throw plutôt que clamp silencieux,\n * cohérent avec `clampLimit`.\n */\nexport const MAX_OFFSET = 100_000;\n/**\n * Bornes radius_km homogènes pour toutes les recherches géographiques (FINESS,\n * Ameli, futurs IRIS). Source unique pour empêcher la dérive entre layers\n * (avant V0.4.1, FINESS DB n'avait aucune validation et acceptait `radiusKm:\n * 1000` côté boundary alors que le tool layer plafonnait à 50).\n */\nexport const RADIUS_MIN_KM = 0.1;\nexport const RADIUS_MAX_KM = 50;\n\n/**\n * Validates and returns a row limit. Throws RangeError outside [1, 500].\n * No clamping (silently capping a 1000-row request to 500 hides the truncation\n * from the caller, which is exactly the kind of silent failure the audit\n * V0.2 flagged).\n */\nexport function clampLimit(limit: number | undefined): number {\n if (limit === undefined) return DEFAULT_LIMIT;\n if (limit < 1 || limit > MAX_LIMIT) {\n throw new RangeError(\n `[france-data-mcp] limit must be between 1 and ${MAX_LIMIT}, got ${limit}`,\n );\n }\n return limit;\n}\n\n/**\n * Validates and returns a pagination offset. Default 0. Throws RangeError if\n * negative, non-finite, or above MAX_OFFSET. Same loud-failure philosophy as\n * `clampLimit` — silently zeroing a -1 offset would mask a caller bug that\n * iterates downward thinking it's going forward.\n */\nexport function clampOffset(offset: number | undefined): number {\n if (offset === undefined) return 0;\n if (!Number.isFinite(offset) || offset < 0 || offset > MAX_OFFSET) {\n throw new RangeError(\n `[france-data-mcp] offset must be between 0 and ${MAX_OFFSET}, got ${offset}`,\n );\n }\n return Math.floor(offset);\n}\n\n/**\n * Validates a search radius in kilometres. Bounds [0.1, 50] : 0.1 km est le\n * plus petit rayon utile (≈ rue), 50 km couvre une aire d'attraction urbaine.\n * Au-delà, ST_DWithin sur 95K rows commence à coûter sans valeur ajoutée\n * (le caller devrait passer en query par département à la place).\n */\nexport function validateRadiusKm(radiusKm: number): void {\n if (!Number.isFinite(radiusKm) || radiusKm < RADIUS_MIN_KM || radiusKm > RADIUS_MAX_KM) {\n throw new RangeError(\n `[france-data-mcp] radiusKm must be in [${RADIUS_MIN_KM}, ${RADIUS_MAX_KM}], got ${radiusKm}`,\n );\n }\n}\n\n/**\n * Validates WGS84 coordinates. PostGIS itself accepts any number, so a\n * caller that swaps lat/lon (e.g. lat=2.6, lon=49.7) silently returns 0\n * results — indistinguishable from \"no data here\". Validating bounds here\n * makes that mistake loud.\n */\nexport function validateCoords(lat: number, lon: number): void {\n if (!Number.isFinite(lat) || lat < -90 || lat > 90) {\n throw new RangeError(`[france-data-mcp] lat must be in [-90, 90], got ${lat}`);\n }\n if (!Number.isFinite(lon) || lon < -180 || lon > 180) {\n throw new RangeError(`[france-data-mcp] lon must be in [-180, 180], got ${lon}`);\n }\n}\n\n/**\n * Formats a Supabase RPC error into a single string preserving the postgres\n * code, hint, and details. Losing those fields turned a \"permission denied\"\n * incident in v0.2.0 into a 30-minute investigation. Always include\n * `error.code` so the operator can grep PgError tables (PGRST205 / 42703 /\n * etc.) directly in logs.\n */\nexport function formatRpcError(\n rpc: string,\n error: { code?: string; message: string; hint?: string; details?: string },\n): string {\n const code = error.code ? ` (${error.code})` : \"\";\n const hint = error.hint ? ` — hint: ${error.hint}` : \"\";\n const details = error.details ? ` — details: ${error.details}` : \"\";\n return `[france-data-mcp] ${rpc}${code}: ${error.message}${details}${hint}`;\n}\n\n/**\n * Trim CHAR-padded fields. Postgres CHAR(N) right-pads with spaces, so a\n * dept \"08\" stored as CHAR(3) comes back as \"08 \". Strip it once at the\n * boundary so callers don't have to special-case the padding.\n */\nexport function trimOrNull(s: string | null | undefined): string | null {\n if (s === null || s === undefined) return null;\n const trimmed = s.trim();\n return trimmed === \"\" ? null : trimmed;\n}\n\n/**\n * Valide un numéro FINESS site (9 chiffres) côté boundary public. Renvoie le\n * `numFiness` trimmed pour que le caller forward la version normalisée à la\n * RPC. Throw `RangeError` (mappé JSON-RPC -32602 par `api/mcp.ts`) sur format\n * invalide. Aligné sur `assertValidDept` (territoire/dept-codes.ts).\n */\nexport function assertValidNumFiness(numFiness: string): string {\n const trimmed = numFiness.trim();\n if (!/^\\d{9}$/.test(trimmed)) {\n throw new RangeError(\n `[france-data-mcp] num_finess invalide \"${numFiness}\" — attendu 9 chiffres (FINESS site).`,\n );\n }\n return trimmed;\n}\n\n/**\n * Normalise le `data` retourné par un RPC supabase-js en array typé.\n *\n * Supabase RPC convention : sur un SETOF, `error == null` ⇒ `data` est un\n * `T[]` (potentiellement vide). Recevoir `data == null` quand `error` est\n * également null signale une violation du contrat (RPC renommé sans erreur,\n * permission silencieuse, glitch côté supabase-js). Le caller bénéficiait\n * jusqu'ici d'un `data ?? []` qui masquait silencieusement ce cas comme un\n * résultat vide — exactement le silent failure que CLAUDE.md interdit.\n *\n * Throw plutôt que log + fallback : un caller LLM voit le throw remonter\n * dans la réponse MCP et peut décider (retry, fallback, abandon).\n */\nexport function expectRpcRows<T>(rpc: string, data: unknown): T[] {\n if (data === null || data === undefined) {\n throw new Error(\n `[france-data-mcp] ${rpc}: RPC contract violation — supabase-js returned no error but data is ${data === null ? \"null\" : \"undefined\"}. Expected an array (possibly empty). Investigate RPC name, schema cache, or supabase-js version.`,\n );\n }\n if (!Array.isArray(data)) {\n throw new Error(\n `[france-data-mcp] ${rpc}: RPC contract violation — expected array, got ${typeof data}. Likely an RPC signature mismatch.`,\n );\n }\n return data as T[];\n}\n","import { type LookupResult, lookupFound, lookupNotFound } from \"../core/lookup-result.js\";\nimport { metersToKm } from \"../core/numbers.js\";\nimport {\n type QueryMetadata,\n finessByCategorieMetadata,\n finessRadiusMetadata,\n} from \"../core/query-metadata.js\";\nimport { getAnonClient } from \"../storage/supabase.js\";\nimport {\n clampLimit,\n expectRpcRows,\n formatRpcError,\n trimOrNull,\n validateCoords,\n validateRadiusKm,\n} from \"./db-helpers.js\";\nimport {\n FINESS_FAMILY_CODES,\n type FinessFamille,\n type FinessFamilleQuery,\n finessFamille,\n} from \"./finess-categories.js\";\n\nexport type { FinessFamilleQuery } from \"./finess-categories.js\";\n\nexport interface FinessResult {\n num_finess: string;\n raison_sociale: string;\n categorie: { code: string | null; libelle: string | null; famille: FinessFamille };\n adresse: {\n voie: string | null;\n code_postal: string | null;\n ville: string | null;\n code_departement: string | null;\n code_insee: string;\n };\n coords: { lat: number; lon: number } | null;\n distance_km: number | null;\n telephone: string | null;\n email: string | null;\n}\n\nexport interface InRadiusInput {\n center: { lat: number; lon: number };\n radiusKm: number;\n familles?: FinessFamilleQuery[];\n limit?: number;\n}\n\nexport interface ByCategorieInput {\n famille: FinessFamilleQuery;\n departement?: string;\n code_insee?: string;\n limit?: number;\n}\n\nexport interface FinessQueryResult {\n count: number;\n truncated: boolean;\n results: FinessResult[];\n /**\n * Métadonnées sur la précision géo et le type de distance. Surface au\n * caller MCP que les coords proviennent du Lambert93 DREES (~adresse) et\n * que la distance est haversine (pas routière). Inclut un rappel sur la\n * latence DREES (~1-2 mois) pour les structures émergentes.\n *\n * Optionnel : tous les RPCs de prod la peuplent (cf. `getFinessInRadius`/\n * `getFinessByCategorie`) ; cas d'absence réservé aux mocks tests.\n */\n query_metadata?: QueryMetadata;\n}\n\nfunction familiesToCodes(familles: FinessFamilleQuery[] | undefined): string[] {\n if (!familles || familles.length === 0) return [];\n return familles.flatMap((f) => [...FINESS_FAMILY_CODES[f]]);\n}\n\n/**\n * Find FINESS establishments within a geographic radius. Spatial query uses\n * PostGIS ST_DWithin on the geography type for accurate kilometers.\n *\n * Implemented via a Postgres RPC (`finess_in_radius`, migration\n * 20260508000004) because supabase-js cannot express ST_DWithin / ST_Distance\n * through its query builder.\n */\nexport async function getFinessInRadius(input: InRadiusInput): Promise<FinessQueryResult> {\n const limit = clampLimit(input.limit);\n validateCoords(input.center.lat, input.center.lon);\n // Avant V0.4.1, le DB layer n'avait aucune validation de rayon — un caller\n // direct (lib npm, pas le MCP) pouvait passer `radiusKm: 1000` et faire\n // tourner ST_DWithin sur 95K rows pour rien. Le tool layer plafonnait, mais\n // le DB layer doit aussi se protéger : c'est lui le boundary public.\n validateRadiusKm(input.radiusKm);\n\n const supabase = getAnonClient();\n const { data, error } = await supabase.rpc(\"finess_in_radius\", {\n p_lat: input.center.lat,\n p_lon: input.center.lon,\n p_radius_meters: input.radiusKm * 1000,\n p_codes: familiesToCodes(input.familles),\n p_limit: limit + 1, // +1 to detect truncation\n });\n\n if (error) {\n throw new Error(formatRpcError(\"finess_in_radius\", error));\n }\n return buildFinessQueryResult(\"finess_in_radius\", data, limit, finessRadiusMetadata());\n}\n\n/**\n * Find FINESS establishments by family (and optional dept / commune filters).\n * No spatial query — pure WHERE on category code list + optional location.\n */\nexport async function getFinessByCategorie(input: ByCategorieInput): Promise<FinessQueryResult> {\n const limit = clampLimit(input.limit);\n\n const supabase = getAnonClient();\n const { data, error } = await supabase.rpc(\"finess_by_categorie\", {\n p_codes: [...FINESS_FAMILY_CODES[input.famille]],\n p_departement: input.departement ?? (null as unknown as string),\n p_code_insee: input.code_insee ?? (null as unknown as string),\n p_limit: limit + 1,\n });\n if (error) {\n throw new Error(formatRpcError(\"finess_by_categorie\", error));\n }\n return buildFinessQueryResult(\"finess_by_categorie\", data, limit, finessByCategorieMetadata());\n}\n\n/**\n * Fetch a single FINESS establishment by its 9-digit FINESS number.\n *\n * Retourne un `LookupResult` discriminé par `found`. Si le numéro n'existe\n * pas dans le dump FINESS DREES (numéro mal formé, fermeture récente non\n * encore propagée, frais d'établissement émergent — la base DREES a 1-2 mois\n * de retard sur le terrain), la fonction renvoie un objet `{ found: false,\n * lookupStatus: \"not_found\", message }` au lieu d'un `null` silencieux.\n * Pattern aligné sur `getEntrepriseBySiren` et `getCommuneByCode`\n * (cf. `src/core/lookup-result.ts`).\n */\nexport async function getFinessByNumFiness(numFiness: string): Promise<LookupResult<FinessResult>> {\n if (!/^\\d{9}$/.test(numFiness)) {\n throw new Error(`[france-data-mcp] num_finess must be 9 digits, got \"${numFiness}\"`);\n }\n const supabase = getAnonClient();\n const { data, error } = await supabase.rpc(\"finess_by_num_finess\", {\n p_num_finess: numFiness,\n });\n if (error) {\n throw new Error(formatRpcError(\"finess_by_num_finess\", error));\n }\n const rows = expectRpcRows<RawFinessRow>(\"finess_by_num_finess\", data);\n if (rows.length > 1) {\n // The RPC has a `LIMIT 1` clause, but defense-in-depth: if a deploy\n // glitch removed it, or if the table somehow had duplicate num_finess\n // (PK is enforced by `finess_staging` but rename-swap relies on\n // discipline), surface the violation loud instead of silently picking\n // the first row.\n console.warn(\n `[france-data-mcp] finess_by_num_finess(${numFiness}): RPC returned ${rows.length} rows (expected ≤ 1) — picking the first. Investigate finess table for duplicate num_finess.`,\n );\n }\n const first = rows[0];\n if (!first) {\n return lookupNotFound(\n numFiness,\n `Numéro FINESS \"${numFiness}\" introuvable dans la base DREES (dernière sync bimestrielle). Causes possibles : numéro inexistant, structure très récente non encore propagée par DREES (latence ~1-2 mois), erreur de saisie. Pour structures émergentes (CPTS, MSP récentes), cross-check avec ARS régionale ou Service Public.`,\n );\n }\n return lookupFound(toFinessResult(first));\n}\n\n// --- internals -------------------------------------------------------------\n\nfunction buildFinessQueryResult(\n rpc: string,\n data: unknown,\n limit: number,\n metadata: QueryMetadata,\n): FinessQueryResult {\n const rows = expectRpcRows<RawFinessRow>(rpc, data);\n const truncated = rows.length > limit;\n const sliced = truncated ? rows.slice(0, limit) : rows;\n return {\n count: sliced.length,\n truncated,\n results: sliced.map(toFinessResult),\n query_metadata: metadata,\n };\n}\n\ninterface RawFinessRow {\n num_finess: string;\n raison_sociale: string;\n categorie_code: string | null;\n categorie_libelle: string | null;\n voie: string | null;\n code_postal: string | null;\n code_departement: string | null;\n code_insee: string;\n ville: string | null;\n telephone: string | null;\n email: string | null;\n geom: { type: \"Point\"; coordinates: [number, number] } | null;\n distance_meters?: number; // present only on RPC result\n}\n\nfunction toFinessResult(row: RawFinessRow): FinessResult {\n const coords = row.geom\n ? { lat: row.geom.coordinates[1] ?? 0, lon: row.geom.coordinates[0] ?? 0 }\n : null;\n return {\n num_finess: row.num_finess,\n raison_sociale: row.raison_sociale,\n categorie: {\n code: row.categorie_code,\n libelle: row.categorie_libelle,\n famille: finessFamille(row.categorie_code),\n },\n adresse: {\n voie: row.voie,\n code_postal: trimOrNull(row.code_postal),\n ville: row.ville,\n code_departement: trimOrNull(row.code_departement),\n code_insee: row.code_insee.trim(),\n },\n coords,\n distance_km: metersToKm(row.distance_meters),\n telephone: trimOrNull(row.telephone),\n email: row.email,\n };\n}\n","/**\n * Code département canonique français — utilities partagées entre les\n * différents callers (commune index, ingestion FINESS, tools MCP).\n *\n * Pourquoi un module dédié : trois variantes existaient avant V0.4 dans\n * `commune-index.ts`, `api/tools.ts`, `scripts/ingest/finess.ts` — chacune\n * avec un edge case manquant (Corse, DOM, longueur min). Centralise pour\n * éviter les divergences silencieuses au prochain ingester.\n */\n\n/**\n * Dérive le code département canonique depuis un code INSEE 5 chars.\n * - Métropole : \"75001\" → \"75\"\n * - Corse : \"2A001\" → \"2A\", \"2B033\" → \"2B\"\n * - DOM/COM : \"97401\" → \"974\", \"98701\" → \"987\"\n *\n * Renvoie `undefined` si le code est trop court pour être interprété\n * (les callers FINESS/Ameli traitent ça comme un skip avec compteur).\n */\nexport function deptFromCodeInsee(codeInsee: string | null | undefined): string | undefined {\n if (!codeInsee || codeInsee.length < 2) return undefined;\n if (codeInsee.startsWith(\"2A\") || codeInsee.startsWith(\"2B\")) return codeInsee.slice(0, 2);\n // DOM/COM codes need 3 chars to be unambiguous — \"97\" alone could be\n // 971-978 ; refuse rather than guess.\n if (codeInsee.startsWith(\"97\") || codeInsee.startsWith(\"98\")) {\n return codeInsee.length >= 3 ? codeInsee.slice(0, 3) : undefined;\n }\n return codeInsee.slice(0, 2);\n}\n\n/**\n * Validateur de code département (cellule CSV ou input MCP). Accepte :\n * - 2 chars métropole : \"01\"-\"95\" (excluant \"20\" — Corse utilise 2A/2B)\n * - \"2A\" / \"2B\" (Corse)\n * - 3 chars DOM/COM : \"971\"-\"978\" (DROM) et \"984\"-\"988\" (COM)\n *\n * Anything else is malformed (column shift, dirty data, user typo) and\n * returns false. Les callers décident s'ils throw ou skip.\n */\nexport function isValidDept(dept: string): boolean {\n if (dept === \"2A\" || dept === \"2B\") return true;\n if (/^\\d{2}$/.test(dept)) return dept !== \"20\";\n if (/^(97[1-8]|98[4-8])$/.test(dept)) return true;\n return false;\n}\n\n/**\n * Variante throw-on-invalid de `isValidDept`. Cohérent avec les autres\n * validators du DB layer (`validateCoords`, `validateRadiusKm`) : `RangeError`\n * pour permettre au boundary MCP de mapper vers JSON-RPC -32602 (Invalid\n * params) au lieu de -32603 (Internal error).\n */\nexport function assertValidDept(dept: string): void {\n if (isValidDept(dept)) return;\n throw new RangeError(`[france-data-mcp] departement must be a valid INSEE code, got \"${dept}\"`);\n}\n\n/**\n * Dérive le code département depuis un code postal 5 chiffres. Sert au\n * fallback de l'ingestion RPPS V0.5.1 quand le CP+ville ne match aucune\n * commune INSEE — on garde au moins le dept pour permettre le filtrage\n * `rpps_par_specialite_dept`.\n *\n * - \"08000\" → \"08\" (métropole : 2 premiers)\n * - \"75001\" → \"75\"\n * - \"97400\" → \"974\" (DROM : 3 premiers, 971-978)\n * - \"98711\" → \"987\" (COM : 3 premiers, 984-988)\n * - \"20100\" → undefined (Corse ambigu : 2A si 20000-20199/20300+, 2B si\n * 20200-20299 — ne pas inventer sans la commune)\n * - moins de 5 chiffres ou non-numérique → undefined\n *\n * Validation finale via `isValidDept` pour ne jamais retourner un code\n * fictif (ex: \"99\" pour un CP \"99xxx\" anormal — non français).\n */\nexport function deriveDeptFromCp(cp: string | null | undefined): string | undefined {\n if (!cp) return undefined;\n const trimmed = cp.trim();\n if (!/^\\d{5}/.test(trimmed)) return undefined;\n const cp5 = trimmed.slice(0, 5);\n if (cp5.startsWith(\"20\")) return undefined;\n const candidate =\n cp5.startsWith(\"97\") || cp5.startsWith(\"98\") ? cp5.slice(0, 3) : cp5.slice(0, 2);\n // Validation stricte locale : on accepte uniquement les ranges réellement\n // émises côté INSEE. `isValidDept` est permissif (regex `\\d{2}` tolère \"96\",\n // \"99\", etc.) pour rétro-compat du DB layer ; ici on dérive depuis du data\n // RPPS brut, autant ne pas stocker de dept fantaisiste qui polluerait\n // `rpps_par_specialite_dept`. Métropole = 01-95 sauf 20 (Corse 2A/2B exclue\n // car déjà filtrée plus haut). DOM = 971-978. COM = 984-988.\n if (/^(0[1-9]|1[0-9]|2[1-9]|[3-8][0-9]|9[0-5])$/.test(candidate)) return candidate;\n if (/^(97[1-8]|98[4-8])$/.test(candidate)) return candidate;\n return undefined;\n}\n","/**\n * Types et nomenclatures RPPS / Annuaire Santé ANS.\n *\n * Sources :\n * - Fichier `ps-libreacces-personne-activite.txt` (data.gouv) — 50 colonnes\n * pipe-delimited. La doc canonique des nomenclatures (codes professions,\n * savoir-faire, mode exercice) vit côté ANS sur :\n * https://annuaire.sante.fr/web/site-pro/extractions-publiques\n *\n * Granularité : une LIGNE du CSV = un (PS, structure d'exercice). Un PS qui\n * exerce sur N sites a N lignes. Le serveur expose les rows brutes ; le caller\n * (ou un futur tool composite) regroupe par `rpps_id` si besoin.\n */\n\n/** GeoJSON Point — shape retournée par `ST_AsGeoJSON(geom)::jsonb`. */\nexport interface GeoJsonPoint {\n type: \"Point\";\n coordinates: [number, number];\n}\n\n/**\n * Codes mode d'exercice ANS (extrait de la nomenclature canonique).\n * Documenté ici pour clarté du caller MCP qui veut filtrer par statut.\n *\n * Source : nomenclature de structure ANS, fichier `nomenclature-mode-exercice`.\n */\nexport const RPPS_MODE_EXERCICE = {\n LIBERAL: \"L\",\n SALARIE: \"S\",\n MIXTE: \"M\",\n REMPLACANT: \"R\",\n AUTRE: \"A\",\n BENEVOLE: \"B\",\n} as const;\n\n/**\n * URL de référence ANS pour les nomenclatures publiques. Mention obligatoire\n * en CGU des datasets data.gouv : « Source : Annuaire Santé, ANS — Licence\n * Ouverte v2.0 ».\n */\nexport const RPPS_CGU_NOTICE =\n \"Source : Annuaire Santé, Agence du Numérique en Santé (ANS) — Licence Ouverte v2.0\";\n\n/**\n * URL canonique de la table de référence ANS TRE_R09 (catégorie professionnelle).\n * Source unique citée par la JSDoc des constantes `CATEGORIE_CODE_*`, par le\n * hint MCP `RPPS_INCLUDE_CATEGORIES_HINT` et par la doc publique. Évite la\n * dérive multi-sites quand l'ANS publie une nouvelle URL.\n */\nexport const TRE_R09_URL = \"https://mos.esante.gouv.fr/NOS/TRE_R09-CategorieProfessionnelle/\";\n","/**\n * RPPS / Annuaire Santé ANS — wrappers typés autour des RPCs PostGIS.\n *\n * Source : data.gouv `annuaire-sante-extractions-...-rpps`, Licence Ouverte v2.0.\n * La mention obligatoire (ANS / Licence Ouverte v2.0) est portée par les\n * descriptions des tools MCP (`api/tools.ts`). Ce module est le boundary\n * technique, pas le boundary public.\n *\n * Diffère d'Ameli sur 3 points :\n * - couverture : libéraux + salariés + étudiants + agents publics\n * (vs Ameli libéraux conventionnés uniquement)\n * - identifiant stable : `rpps_id` (IDNPS national) → lookup individuel + dédup\n * - pivot structure : `num_finess` exposé en colonne → croisement avec FINESS\n *\n * IMPORTANT : la base ne contient QUE des PS actifs. L'ANS pré-filtre le\n * fichier `PS_LibreAcces_Personne_activite` à la source : retraités, décédés,\n * radiés et suspendus n'apparaissent jamais dans cette extraction (cf. DSFT\n * v3.1 §5.1.2). Le filtre par `categorie_code` discrimine donc des **statuts\n * juridiques d'enregistrement** (Civil / Étudiant / Agent public), pas des\n * statuts d'activité.\n */\n\nimport { metersToKm } from \"../core/numbers.js\";\nimport {\n type QueryMetadata,\n rppsDeptMetadata,\n rppsEtablissementMetadata,\n rppsRadiusMetadata,\n rppsSearchByNameMetadata,\n} from \"../core/query-metadata.js\";\nimport { getUntypedAnonClient } from \"../storage/supabase.js\";\nimport { assertValidDept } from \"../territoire/dept-codes.js\";\nimport {\n clampLimit,\n clampOffset,\n expectRpcRows,\n formatRpcError,\n trimOrNull,\n validateCoords,\n validateRadiusKm,\n} from \"./db-helpers.js\";\nimport { type GeoJsonPoint, TRE_R09_URL } from \"./rpps-types.js\";\n\n// --- Public result shapes --------------------------------------------------\n\nexport interface RppsResult {\n id: number;\n rpps_id: string;\n identite: {\n nom: string;\n prenom: string;\n civilite: string | null;\n };\n profession: { code: string | null; libelle: string | null };\n /** Spécialité fine (DES/DESC). Plus riche que la spécialité Ameli simple. */\n savoir_faire: { code: string | null; libelle: string | null };\n mode_exercice: { code: string | null; libelle: string | null };\n /** Catégorie professionnelle ANS (TRE_R09) — voir `CATEGORIE_CODE_*` / `buildCategorieCodes`. */\n categorie: { code: string | null; libelle: string | null };\n /** Pivot vers FINESS / SIRENE. Souvent rempli pour les salariés, plus rare en libéral pur. */\n structure: {\n num_finess: string | null;\n num_finess_ej: string | null;\n siret: string | null;\n raison_sociale: string | null;\n };\n adresse: {\n voie: string | null;\n code_postal: string | null;\n ville: string | null;\n code_departement: string | null;\n code_insee: string | null;\n };\n coords: { lat: number; lon: number } | null;\n distance_km: number | null;\n telephone: string | null;\n /**\n * Score de pertinence trigram (0..1) — présent uniquement pour les retours\n * de `rpps_search_by_name`. Permet au caller de filtrer les homonymies\n * partielles (typiquement `< 0.5`).\n */\n match_score?: number;\n}\n\nexport interface RppsLookupResult extends RppsResult {\n /** Identifiant PP legacy (pré-IDNPS), conservé quand fourni par l'extract. */\n identifiant_pp: string | null;\n siren: string | null;\n email: string | null;\n}\n\nexport interface RppsInRadiusInput {\n center: { lat: number; lon: number };\n radiusKm: number;\n /** Codes profession ANS (ex: \"10\" Médecin, \"60\" Infirmier). */\n professionCodes?: string[];\n /** Codes savoir-faire (DES/DESC). Granularité fine. */\n savoirFaireCodes?: string[];\n /** Codes mode exercice (L libéral, S salarié, M mixte, R remplaçant…). */\n modeExerciceCodes?: string[];\n /**\n * Codes catégorie professionnelle ANS (table TRE_R09). Vide ou omis →\n * filtre default = `[CATEGORIE_CODE_CIVIL]` (cf. `buildCategorieCodes`).\n * Sinon → filtre exact ANY (le helper SQL `rpps_categorie_match` ajoute\n * `OR IS NULL` défensif pour ne pas exclure les rows à code absent).\n */\n categorieCodes?: string[];\n limit?: number;\n}\n\nexport interface RppsParSpecialiteDeptInput {\n departement: string;\n professionCode?: string;\n savoirFaireCode?: string;\n modeExerciceCode?: string;\n /** Voir `RppsInRadiusInput.categorieCodes`. */\n categorieCodes?: string[];\n limit?: number;\n offset?: number;\n}\n\nexport interface RppsDansEtablissementInput {\n /** Numéro FINESS (9 chiffres) du site d'exercice. */\n numFiness: string;\n /** Voir `RppsInRadiusInput.categorieCodes`. */\n categorieCodes?: string[];\n limit?: number;\n}\n\nexport interface RppsSearchByNameInput {\n /** Nom de famille (obligatoire, non vide après trim). */\n nom: string;\n /** Prénom (optionnel — sans, le matching ne porte que sur le nom). */\n prenom?: string;\n /** Code département (2 chiffres métropole/Corse, 3 pour DOM). Optionnel. */\n departement?: string;\n /**\n * Codes catégorie ANS TRE_R09. Vide ou omis → default `[C]` (Civil seul),\n * cohérent avec les 3 autres tools RPPS.\n */\n categorieCodes?: string[];\n limit?: number;\n}\n\n/**\n * Codes catégorie professionnelle ANS — table de référence TRE_R09 (cf.\n * `TRE_R09_URL`). Le code `F` déprécié 2026-02-23 a été fusionné dans `M`,\n * et le fichier `PS_LibreAcces_Personne_activite` est pré-filtré aux actifs\n * à la source — d'où l'absence de codes `R`/`S`/`D` (cf. JSDoc de tête).\n */\nexport const CATEGORIE_CODE_CIVIL = \"C\";\nexport const CATEGORIE_CODE_ETUDIANT = \"E\";\nexport const CATEGORIE_CODE_AGENT_PUBLIC = \"M\";\n\n/** Codes valides dans TRE_R09 actuellement présents en base. */\nexport const CATEGORIE_CODES_OFFICIELS = Object.freeze([\n CATEGORIE_CODE_CIVIL,\n CATEGORIE_CODE_ETUDIANT,\n CATEGORIE_CODE_AGENT_PUBLIC,\n] as const);\n\n/**\n * Default appliqué TS-side dans `getRppsParSpecialiteDept`. La RPC V0.5.4\n * (`EXECUTE format`) porte aussi son propre `COALESCE(... ARRAY['C'])` en\n * défense — KEEP IN SYNC si on change le default.\n */\nexport const CATEGORIE_CODES_DEFAUT = Object.freeze([\n CATEGORIE_CODE_CIVIL,\n] as const) satisfies readonly string[];\n\n/**\n * Construit `categorieCodes` à partir des 2 flags MCP. Source unique\n * consommée par les 3 handlers tools.\n */\nexport function buildCategorieCodes(opts: {\n includeEtudiants?: boolean;\n includeAgentsPublics?: boolean;\n}): string[] {\n const codes: string[] = [CATEGORIE_CODE_CIVIL];\n if (opts.includeAgentsPublics) codes.push(CATEGORIE_CODE_AGENT_PUBLIC);\n if (opts.includeEtudiants) codes.push(CATEGORIE_CODE_ETUDIANT);\n return codes;\n}\n\n/** Référence stable de la nomenclature ANS. Alias re-exporté pour la doc. */\nexport { TRE_R09_URL };\n\nexport interface RppsQueryResult {\n count: number;\n truncated: boolean;\n results: RppsResult[];\n query_metadata?: QueryMetadata;\n}\n\n// --- Public query functions ------------------------------------------------\n\nexport async function getRppsInRadius(input: RppsInRadiusInput): Promise<RppsQueryResult> {\n const limit = clampLimit(input.limit);\n validateCoords(input.center.lat, input.center.lon);\n validateRadiusKm(input.radiusKm);\n\n const supabase = getUntypedAnonClient();\n const { data, error } = await supabase.rpc(\"rpps_in_radius\", {\n p_lat: input.center.lat,\n p_lon: input.center.lon,\n p_radius_meters: input.radiusKm * 1000,\n p_profession_codes: input.professionCodes ?? [],\n p_savoir_faire_codes: input.savoirFaireCodes ?? [],\n p_mode_exercice_codes: input.modeExerciceCodes ?? [],\n p_categorie_codes: input.categorieCodes ?? [],\n p_limit: limit + 1,\n });\n\n if (error) throw new Error(formatRpcError(\"rpps_in_radius\", error));\n return buildQueryResult(\"rpps_in_radius\", data, limit, rppsRadiusMetadata());\n}\n\nexport async function getRppsParSpecialiteDept(\n input: RppsParSpecialiteDeptInput,\n): Promise<RppsQueryResult> {\n const limit = clampLimit(input.limit);\n const offset = clampOffset(input.offset);\n assertValidDept(input.departement);\n\n const supabase = getUntypedAnonClient();\n // Le client untyped ne contraint pas les types des params RPC — on peut\n // passer `null` directement pour les filtres optionnels (le RPC PostgreSQL\n // gère `NULL → pas de filtre` via `IS NULL OR ... = ...`).\n // `categorieCodes` vide ou omis → default = `[C]` (Civil seul). La RPC\n // V0.5.4 (LANGUAGE plpgsql + EXECUTE format) accepte `[]` et retombe sur\n // son propre default `['C']` côté SQL, mais on explicite ici pour rester\n // cohérent avec les 2 autres callers et faciliter le debug.\n const categorieCodes =\n input.categorieCodes && input.categorieCodes.length > 0\n ? input.categorieCodes\n : [...CATEGORIE_CODES_DEFAUT];\n const { data, error } = await supabase.rpc(\"rpps_par_specialite_dept\", {\n p_departement: input.departement,\n p_profession_code: input.professionCode ?? null,\n p_savoir_faire_code: input.savoirFaireCode ?? null,\n p_mode_exercice_code: input.modeExerciceCode ?? null,\n p_categorie_codes: categorieCodes,\n p_limit: limit + 1,\n p_offset: offset,\n });\n\n if (error) throw new Error(formatRpcError(\"rpps_par_specialite_dept\", error));\n return buildQueryResult(\"rpps_par_specialite_dept\", data, limit, rppsDeptMetadata());\n}\n\n/** \"Qui travaille dans ce FINESS ?\" — lit la colonne indexée `num_finess`. */\nexport async function getRppsDansEtablissement(\n input: RppsDansEtablissementInput,\n): Promise<RppsQueryResult> {\n const limit = clampLimit(input.limit);\n const numFiness = input.numFiness.trim();\n if (!/^\\d{9}$/.test(numFiness)) {\n throw new RangeError(\n `[france-data-mcp] num_finess invalide \"${input.numFiness}\" — attendu 9 chiffres (FINESS site).`,\n );\n }\n\n const supabase = getUntypedAnonClient();\n const { data, error } = await supabase.rpc(\"rpps_dans_etablissement\", {\n p_num_finess: numFiness,\n p_categorie_codes: input.categorieCodes ?? [],\n p_limit: limit + 1,\n });\n\n if (error) throw new Error(formatRpcError(\"rpps_dans_etablissement\", error));\n const rows = expectRpcRows<RawRppsCompactRow>(\"rpps_dans_etablissement\", data);\n const truncated = rows.length > limit;\n const sliced = truncated ? rows.slice(0, limit) : rows;\n return {\n count: sliced.length,\n truncated,\n results: sliced.map(toCompactResult),\n query_metadata: rppsEtablissementMetadata(),\n };\n}\n\n/**\n * Recherche fuzzy par identité (nom, prenom?, departement?). Utilise pg_trgm\n * `similarity()` côté SQL avec index GIN trigram sur `lower(nom)` et\n * `lower(prenom)` (migration `20260511T100000_rpps_search_by_name`). Tri par\n * score décroissant.\n *\n * Comportement edge cases :\n * - `nom` vide ou whitespace → throw `RangeError` (validation côté SQL aussi)\n * - `departement` mal formé → throw via la RPC (ERRCODE 22023)\n * - aucune correspondance → `{ count: 0, results: [] }`\n */\nexport async function getRppsByName(input: RppsSearchByNameInput): Promise<RppsQueryResult> {\n const nom = input.nom.trim();\n if (nom.length === 0) {\n throw new RangeError(\n \"[france-data-mcp] rpps_search_by_name: nom est requis (non vide après trim).\",\n );\n }\n const prenom = input.prenom?.trim();\n const limit = clampLimit(input.limit);\n if (input.departement !== undefined) assertValidDept(input.departement);\n // Default `[C]` (Civil seul) cohérent avec les 3 autres tools RPPS — un\n // caller cherchant un PS par nom récupère par défaut les libéraux + salariés\n // privés + hospitaliers contractuels, pas les étudiants ni les agents publics.\n const categorieCodes =\n input.categorieCodes && input.categorieCodes.length > 0\n ? input.categorieCodes\n : [...CATEGORIE_CODES_DEFAUT];\n\n const supabase = getUntypedAnonClient();\n const { data, error } = await supabase.rpc(\"rpps_search_by_name\", {\n p_nom: nom,\n // RPC accepte NULL pour \"pas de filtre prenom\". `??` couvre prenom omis\n // (undefined) ET vide après trim (chaîne vide).\n p_prenom: prenom && prenom.length > 0 ? prenom : null,\n p_departement: input.departement ?? null,\n p_categorie_codes: categorieCodes,\n p_limit: limit + 1,\n });\n\n if (error) throw new Error(formatRpcError(\"rpps_search_by_name\", error));\n const rows = expectRpcRows<RawRppsSearchRow>(\"rpps_search_by_name\", data);\n const truncated = rows.length > limit;\n const sliced = truncated ? rows.slice(0, limit) : rows;\n return {\n count: sliced.length,\n truncated,\n results: sliced.map(toSearchResult),\n query_metadata: rppsSearchByNameMetadata(),\n };\n}\n\n/**\n * Lookup individuel par RPPS ID. Renvoie N rows quand un PS multi-sites\n * existe (1 ligne par site). Le caller MCP aplatit en `(rpps_id, sites[])`.\n */\nexport async function getRppsById(rppsId: string): Promise<RppsLookupResult[]> {\n const trimmed = rppsId.trim();\n if (!/^\\d{11,12}$/.test(trimmed)) {\n throw new RangeError(\n `[france-data-mcp] rpps_id invalide \"${rppsId}\" — attendu 11 ou 12 chiffres (IDNPS national, format ANS — préfixe \"81\" optionnel pour les IDs émis depuis 2020 = 12 chars, sans préfixe = 11 chars).`,\n );\n }\n const supabase = getUntypedAnonClient();\n const { data, error } = await supabase.rpc(\"rpps_lookup_by_id\", {\n p_rpps_id: trimmed,\n });\n if (error) throw new Error(formatRpcError(\"rpps_lookup_by_id\", error));\n const rows = expectRpcRows<RawRppsLookupRow>(\"rpps_lookup_by_id\", data);\n return rows.map(toLookupResult);\n}\n\n// --- internals -------------------------------------------------------------\n\nfunction buildQueryResult(\n rpc: string,\n data: unknown,\n limit: number,\n metadata: QueryMetadata,\n): RppsQueryResult {\n const rows = expectRpcRows<RawRppsRow>(rpc, data);\n const truncated = rows.length > limit;\n const sliced = truncated ? rows.slice(0, limit) : rows;\n return {\n count: sliced.length,\n truncated,\n results: sliced.map(toResult),\n query_metadata: metadata,\n };\n}\n\ninterface RawRppsRow {\n id: number;\n rpps_id: string;\n civilite: string | null;\n nom: string;\n prenom: string;\n profession_code: string | null;\n profession_libelle: string | null;\n savoir_faire_code: string | null;\n savoir_faire_libelle: string | null;\n mode_exercice_code: string | null;\n mode_exercice_libelle: string | null;\n categorie_code: string | null;\n categorie_libelle: string | null;\n num_finess: string | null;\n num_finess_ej: string | null;\n siret: string | null;\n raison_sociale: string | null;\n adresse: string | null;\n code_postal: string | null;\n ville: string | null;\n code_departement: string | null;\n code_insee: string | null;\n telephone: string | null;\n geom: GeoJsonPoint | null;\n distance_meters?: number | null;\n}\n\ninterface RawRppsCompactRow {\n id: number;\n rpps_id: string;\n civilite: string | null;\n nom: string;\n prenom: string;\n profession_code: string | null;\n profession_libelle: string | null;\n savoir_faire_code: string | null;\n savoir_faire_libelle: string | null;\n mode_exercice_code: string | null;\n mode_exercice_libelle: string | null;\n categorie_code: string | null;\n categorie_libelle: string | null;\n num_finess: string | null;\n num_finess_ej: string | null;\n raison_sociale: string | null;\n telephone: string | null;\n}\n\ninterface RawRppsLookupRow extends RawRppsRow {\n identifiant_pp: string | null;\n siren: string | null;\n email: string | null;\n}\n\ninterface RawRppsSearchRow extends RawRppsRow {\n /** Score trigram pg_trgm (0..1) — voir migration `20260511T100000_rpps_search_by_name`. */\n match_score: number | null;\n}\n\nfunction toResult(row: RawRppsRow): RppsResult {\n // Si geom est présent mais coordinates malformé (entry undefined), on retombe\n // explicitement sur null plutôt qu'un (0, 0) golfe de Guinée silencieux.\n const lat = row.geom?.coordinates[1];\n const lon = row.geom?.coordinates[0];\n const coords = typeof lat === \"number\" && typeof lon === \"number\" ? { lat, lon } : null;\n return {\n id: row.id,\n rpps_id: row.rpps_id,\n identite: {\n nom: row.nom,\n prenom: row.prenom,\n civilite: row.civilite,\n },\n profession: { code: row.profession_code, libelle: row.profession_libelle },\n savoir_faire: { code: row.savoir_faire_code, libelle: row.savoir_faire_libelle },\n mode_exercice: { code: row.mode_exercice_code, libelle: row.mode_exercice_libelle },\n categorie: { code: row.categorie_code, libelle: row.categorie_libelle },\n structure: {\n num_finess: row.num_finess,\n num_finess_ej: row.num_finess_ej,\n siret: row.siret,\n raison_sociale: row.raison_sociale,\n },\n adresse: {\n voie: row.adresse,\n // CHAR(N) Postgres pad avec espaces — trim systématique pour ne pas\n // leak `\"08 \"` côté caller (cohérent finess-db.ts / ameli-db.ts).\n code_postal: trimOrNull(row.code_postal),\n ville: row.ville,\n code_departement: trimOrNull(row.code_departement),\n code_insee: trimOrNull(row.code_insee),\n },\n coords,\n distance_km: metersToKm(row.distance_meters),\n telephone: row.telephone,\n };\n}\n\nfunction toCompactResult(row: RawRppsCompactRow): RppsResult {\n return {\n id: row.id,\n rpps_id: row.rpps_id,\n identite: { nom: row.nom, prenom: row.prenom, civilite: row.civilite },\n profession: { code: row.profession_code, libelle: row.profession_libelle },\n savoir_faire: { code: row.savoir_faire_code, libelle: row.savoir_faire_libelle },\n mode_exercice: { code: row.mode_exercice_code, libelle: row.mode_exercice_libelle },\n categorie: { code: row.categorie_code, libelle: row.categorie_libelle },\n structure: {\n num_finess: row.num_finess,\n num_finess_ej: row.num_finess_ej,\n siret: null,\n raison_sociale: row.raison_sociale,\n },\n adresse: {\n voie: null,\n code_postal: null,\n ville: null,\n code_departement: null,\n code_insee: null,\n },\n coords: null,\n distance_km: null,\n telephone: row.telephone,\n };\n}\n\nfunction toLookupResult(row: RawRppsLookupRow): RppsLookupResult {\n // `categorie` est désormais porté par RppsResult (V0.5.1) — hérité via spread.\n return {\n ...toResult(row),\n identifiant_pp: row.identifiant_pp,\n siren: row.siren,\n email: row.email,\n };\n}\n\nfunction toSearchResult(row: RawRppsSearchRow): RppsResult {\n // `match_score` est ajouté uniquement quand la RPC l'a calculé (numeric\n // valide). Si la RPC renvoie `null` (cas dégénéré improbable), on omet le\n // champ plutôt que de leak un `match_score: null` côté caller MCP.\n const base = toResult(row);\n if (typeof row.match_score === \"number\" && Number.isFinite(row.match_score)) {\n return { ...base, match_score: row.match_score };\n }\n return base;\n}\n","/**\n * Annuaire Santé ANS — fallback FHIR live (libre accès, depuis avril 2025).\n *\n * Pourquoi : la table `rpps` est ingérée mensuellement depuis le CSV\n * data.gouv. Pour les SP qui ne sont pas encore dans le snapshot DB (récente\n * inscription, mutation de structure non répercutée), on offre un fallback\n * live via l'API FHIR ANS qui est rafraîchie quotidiennement côté ANS.\n *\n * Auth (vérifié 2026-05-09 sur portail.openfhir.annuaire.sante.fr) :\n * - Header **`ESANTE-API-KEY: <api-key>`** (UUID issu d'une souscription\n * Gravitee gratuite sur portal.api.esante.gouv.fr).\n * - Endpoint : `GET https://gateway.api.esante.gouv.fr/fhir/v2/Practitioner?identifier=...`\n * - Pas de quota documenté pendant la bêta (publique depuis avril 2025) ;\n * limits annoncées « après fin 2025 ».\n *\n * Identifiants : l'API expose `Practitioner.identifier` typé `IDNPS`\n * (système OID `urn:oid:1.2.250.1.71.4.2.1`). Le CSV legacy expose le\n * même champ sous \"Identification nationale PP\" (11 ou 12 chars selon que\n * le préfixe Type d'identifiant `81` est concaténé ou pas — IDNPS modernes\n * = 12 chars, anciens = 11). On cherche par IDNPS qui matche le `rpps_id`\n * que l'on stocke en DB.\n *\n * No-op gracieux : pas de clé → null sans throw — la lib reste utilisable\n * sans clé ANS (la couverture DB suffit à la majorité des cas).\n */\n\nimport { HttpError, fetchJson } from \"../core/http.js\";\n\nconst ANS_FHIR_DEFAULT_BASE = \"https://gateway.api.esante.gouv.fr/fhir/v2\";\nconst ANS_AUTH_HEADER = \"ESANTE-API-KEY\";\nconst IDNPS_SYSTEM = \"urn:oid:1.2.250.1.71.4.2.1\";\n/**\n * Couvre les retries cumulés `fetchJson` (4 tentatives, backoff exponentiel\n * ~7.5s) avec marge confortable même sous lenteur ANS 5xx. Aligné sur le\n * timeout INSEE (V0.4.5) pour un comportement homogène.\n */\nconst FETCH_TIMEOUT_MS = 60_000;\n\nexport function getAnsFhirApiKey(): string | null {\n const raw = process.env.ANS_FHIR_API_KEY;\n if (!raw) return null;\n // Strippe quotes entourants (parsers .env qui les conservent → 401 silencieux),\n // pattern identique à `getInseeApiKey` (cf. insee-sirene.ts V0.4.5).\n const cleaned = raw.trim().replace(/^[\"']|[\"']$/g, \"\");\n return cleaned === \"\" ? null : cleaned;\n}\n\n/** Override optionnel (env de staging, mock test). Sinon endpoint officiel. */\nexport function getAnsFhirBaseUrl(): string {\n const raw = process.env.ANS_FHIR_BASE_URL?.trim();\n return raw && raw !== \"\" ? raw.replace(/\\/+$/, \"\") : ANS_FHIR_DEFAULT_BASE;\n}\n\n/**\n * Shape minimale d'une ressource FHIR Practitioner. On ne lit que ce dont on\n * a besoin pour mapper vers `AnsFhirPractitioner` côté caller. Les autres\n * champs FHIR (qualification, telecom, address, language…) sont ignorés au\n * boundary pour ne pas exploser la surface de typage.\n */\ninterface FhirIdentifier {\n use?: string;\n system?: string;\n value?: string;\n type?: { coding?: Array<{ system?: string; code?: string }> };\n}\n\ninterface FhirHumanName {\n use?: string;\n family?: string;\n given?: string[];\n prefix?: string[];\n}\n\ninterface FhirPractitionerResource {\n resourceType: \"Practitioner\";\n id?: string;\n identifier?: FhirIdentifier[];\n name?: FhirHumanName[];\n active?: boolean;\n}\n\ninterface FhirBundle {\n resourceType: \"Bundle\";\n type?: string;\n total?: number;\n entry?: Array<{ resource?: FhirPractitionerResource }>;\n}\n\n/**\n * Résultat aplati d'un lookup ANS — surface minimaliste pour ne pas dupliquer\n * la richesse FHIR. Le caller MCP (tool `professionnel_by_rpps`) injecte\n * cette shape quand la DB ne trouve pas le PS.\n */\nexport interface AnsFhirPractitioner {\n /** ID interne ANS (format `003-NNNN-NNNN`). Distinct de l'IDNPS. */\n ans_internal_id: string;\n /** IDNPS / RPPS national (11 ou 12 chars selon génération). Identique à `rpps_id` côté DB. */\n rpps_id: string;\n civilite: string | null;\n nom: string;\n prenom: string;\n active: boolean | null;\n source: \"ans_fhir\";\n}\n\n/**\n * Résultat discriminé d'un lookup ANS FHIR. **V0.7.0 breaking** — avant V0.7.0,\n * la fonction retournait `null` indifféremment pour 4 cas (pas de clé, format\n * invalide, PS absent, API down), masquant l'information critique au caller.\n *\n * - `found: true` → `practitioner` peuplé, source ANS live.\n * - `status: \"no_key\"` → clé `ESANTE-API-KEY` non configurée côté serveur.\n * Fallback indisponible — le caller doit s'appuyer sur la DB locale.\n * - `status: \"invalid_format\"` → `rpps_id` rejeté par la garde format\n * (11 ou 12 chiffres requis). Pas d'I/O réseau.\n * - `status: \"not_found\"` → ANS a répondu, PS réellement absent (Bundle vide\n * ou 404). Retry inutile, PS pas inscrit à l'ANS.\n * - `status: \"api_error\"` → ANS indisponible (5xx, timeout, 401, 403, network).\n * Retry justifié dans quelques minutes.\n */\nexport type AnsFhirLookupResult =\n | { found: true; practitioner: AnsFhirPractitioner }\n | {\n found: false;\n status: \"no_key\" | \"invalid_format\" | \"not_found\" | \"api_error\";\n message: string;\n };\n\n/**\n * Lookup FHIR ANS par IDNPS / rpps_id. Utilisé en fallback du lookup DB.\n * Latence p99 ~1-2s. Pas de cache local (ANS est rafraîchi quotidiennement).\n *\n * Retourne un `AnsFhirLookupResult` discriminé — voir le type pour les 5 cas\n * possibles. **Aucun `null` silencieux** (V0.7.0 breaking, cf. JSDoc du type).\n */\nexport async function lookupPractitionerByRpps(rppsId: string): Promise<AnsFhirLookupResult> {\n const apiKey = getAnsFhirApiKey();\n if (!apiKey) {\n return {\n found: false,\n status: \"no_key\",\n message:\n \"ESANTE-API-KEY non configurée côté serveur. Fallback FHIR ANS indisponible — s'appuyer sur la DB locale (snapshot mensuel J-30 max).\",\n };\n }\n\n const trimmed = rppsId.trim();\n if (trimmed === \"\" || !/^\\d{11,12}$/.test(trimmed)) {\n console.warn(\n `[france-data-mcp] ANS FHIR lookup skipped — rpps_id \"${rppsId}\" rejeté par la garde format /^\\\\d{11,12}$/.`,\n );\n return {\n found: false,\n status: \"invalid_format\",\n message: `rpps_id \"${rppsId}\" invalide — format IDNPS attendu : 11 ou 12 chiffres.`,\n };\n }\n\n const baseUrl = getAnsFhirBaseUrl();\n // FHIR search syntax : `identifier=<system>|<value>` cible le `Practitioner`\n // dont l'identifier (use=official) matche IDNPS=trimmed. Plus précis qu'une\n // recherche libre par nom — l'IDNPS est par contrat unique côté ANS.\n const url = `${baseUrl}/Practitioner?identifier=${encodeURIComponent(IDNPS_SYSTEM)}|${encodeURIComponent(trimmed)}`;\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n let bundle: FhirBundle;\n try {\n bundle = await fetchJson<FhirBundle>(url, {\n headers: {\n [ANS_AUTH_HEADER]: apiKey,\n Accept: \"application/fhir+json\",\n },\n signal: controller.signal,\n });\n } catch (err) {\n // Pattern identique à insee-sirene.ts : un seul console.error en début\n // de catch, log différencié 404 (warn, outcome attendu) vs reste (error).\n const httpStatus = err instanceof HttpError ? err.status : null;\n const errMsg = err instanceof Error ? err.message : String(err);\n const detail = httpStatus !== null ? `HTTP ${httpStatus}` : `network/parse error: ${errMsg}`;\n const logFn = httpStatus === 404 ? console.warn : console.error;\n logFn(`[france-data-mcp] ANS FHIR lookup terminated for rpps=${trimmed} — ${detail}`);\n // 404 = identifier vraiment inconnu ANS → `not_found`. Tout autre statut\n // (5xx, 401, 403, network, timeout) = panne API → `api_error` justifie un\n // retry caller. Distinction critique pour ne pas mentir au caller LLM.\n if (httpStatus === 404) {\n return {\n found: false,\n status: \"not_found\",\n message: `IDNPS ${trimmed} introuvable côté ANS FHIR (HTTP 404).`,\n };\n }\n return {\n found: false,\n status: \"api_error\",\n message: `ANS FHIR lookup failed for rpps=${trimmed} — ${detail}. Retry recommandé.`,\n };\n } finally {\n clearTimeout(timeout);\n }\n\n // Bundle vide = identifier inconnu côté ANS. Outcome légitime (PS pas encore\n // inscrit, identifier corrompu côté caller). Pas un incident.\n const entries = bundle.entry ?? [];\n if (entries.length === 0) {\n return {\n found: false,\n status: \"not_found\",\n message: `IDNPS ${trimmed} introuvable côté ANS FHIR (Bundle vide).`,\n };\n }\n\n // FHIR garantit l'unicité par identifier=IDNPS, donc on prend le premier.\n // Les éventuels doublons côté ANS seraient une anomalie qu'on n'essaie pas\n // de résoudre côté client (signaler à ans-annuaire@esante.gouv.fr).\n const resource = entries[0]?.resource;\n if (!resource || resource.resourceType !== \"Practitioner\") {\n console.warn(\n `[france-data-mcp] ANS FHIR Practitioner attendu mais resource=${resource?.resourceType ?? \"null\"} pour rpps=${trimmed} — réponse incohérente`,\n );\n return {\n found: false,\n status: \"api_error\",\n message: `ANS FHIR a renvoyé un payload incohérent pour ${trimmed} (resourceType inattendu).`,\n };\n }\n\n return { found: true, practitioner: mapPractitioner(resource, trimmed) };\n}\n\nfunction mapPractitioner(\n resource: FhirPractitionerResource,\n expectedRpps: string,\n): AnsFhirPractitioner {\n const ansInternalId = resource.id ?? \"\";\n\n // Recherche du IDNPS dans les identifiers — on a filtré côté URL mais on\n // re-vérifie pour ne pas mapper un Practitioner qui aurait un autre\n // identifier en premier (FHIR n'ordonne pas les identifiers).\n const idnps = resource.identifier?.find(\n (id) => id.system === IDNPS_SYSTEM || id.type?.coding?.some((c) => c.code === \"IDNPS\"),\n );\n const idnpsValue = idnps?.value?.trim();\n // Log si on doit fallback : signe d'une régression côté ANS (Practitioner\n // sans IDNPS alors que la requête a matché par identifier). Permet de\n // détecter une dégradation systémique en grep des logs Vercel/CI.\n if (!idnpsValue) {\n console.warn(\n `[france-data-mcp] ANS FHIR Practitioner (id=${resource.id ?? \"?\"}) sans IDNPS exploitable — fallback sur la valeur URL \"${expectedRpps}\"`,\n );\n }\n const rpps_id = idnpsValue || expectedRpps;\n\n // FHIR HumanName : on prend le `name[use=official]` quand disponible, sinon\n // le premier name présent. La civilité est dans `prefix[0]` (Dr, M., Mme).\n const officialName = resource.name?.find((n) => n.use === \"official\") ?? resource.name?.[0];\n const family = officialName?.family?.trim() ?? \"\";\n const given = officialName?.given?.[0]?.trim() ?? \"\";\n const civilite = officialName?.prefix?.[0]?.trim() ?? null;\n\n return {\n ans_internal_id: ansInternalId,\n rpps_id,\n civilite,\n nom: family,\n prenom: given,\n active: typeof resource.active === \"boolean\" ? resource.active : null,\n source: \"ans_fhir\",\n };\n}\n","/**\n * Module sante — données françaises de santé publique.\n *\n * Sources :\n * - DINUM Recherche Entreprises → entreprises secteur santé (live API)\n * - FINESS (data.gouv) → établissements sanitaires et médico-sociaux (dump CSV bimestriel)\n * - Annuaire Santé Ameli (data.gouv/CNAM) → PS libéraux conventionnés (dump CSV hebdo)\n * - RPPS / Annuaire Santé ANS (data.gouv) → tous les PS (libéraux + salariés), ID stable, dump CSV mensuel\n * - FHIR ANS live → fallback fraîcheur quotidienne pour lookup individuel par RPPS ID\n */\n\nexport {\n searchEntreprises,\n getEntrepriseBySiren,\n type Entreprise,\n type Etablissement,\n type Finance,\n type Dirigeant,\n type SearchEntreprisesOptions,\n type SearchEntreprisesResult,\n} from \"./dinum.js\";\n\nexport { getInseeApiKey, lookupSirenViaInsee } from \"./insee-sirene.js\";\n\nexport {\n loadFiness,\n searchEtablissementsFiness,\n haversineDistance,\n type EtablissementFiness,\n type LoadFinessOptions,\n type SearchFinessOptions,\n} from \"./finess.js\";\n\nexport {\n ensureAnnuaireAmeli,\n streamProfessionnels,\n loadProfessionnels,\n type ProfessionnelSante,\n type FilterAnnuaireOptions,\n type StreamAnnuaireOptions,\n} from \"./annuaire-ameli.js\";\n\nexport {\n NAF_SANTE,\n NAF_LABOS,\n NAF_PHARMACIES,\n NAF_EHPAD,\n NAF_MEDECINE_VILLE,\n libelleNaf,\n type NafCodeSante,\n} from \"./naf-codes.js\";\n\nexport {\n FINESS_CATEGORIES,\n FINESS_FAMILY_CODES,\n FINESS_HOPITAUX,\n FINESS_LABOS,\n FINESS_PHARMACIES,\n FINESS_EHPAD,\n FINESS_MSP_CPTS,\n libelleCategorieFiness,\n finessFamille,\n type FinessCategorieCode,\n type FinessFamille,\n} from \"./finess-categories.js\";\n\nexport {\n getFinessInRadius,\n getFinessByCategorie,\n getFinessByNumFiness,\n type FinessResult,\n type FinessQueryResult,\n type InRadiusInput,\n type ByCategorieInput,\n type FinessFamilleQuery,\n} from \"./finess-db.js\";\n\nexport {\n getRppsInRadius,\n getRppsParSpecialiteDept,\n getRppsDansEtablissement,\n getRppsById,\n type RppsResult,\n type RppsLookupResult,\n type RppsQueryResult,\n type RppsInRadiusInput,\n type RppsParSpecialiteDeptInput,\n type RppsDansEtablissementInput,\n} from \"./rpps-db.js\";\n\nexport { RPPS_CGU_NOTICE, RPPS_MODE_EXERCICE } from \"./rpps-types.js\";\n\nexport {\n getAnsFhirApiKey,\n getAnsFhirBaseUrl,\n lookupPractitionerByRpps,\n type AnsFhirPractitioner,\n} from \"./ans-fhir.js\";\n\nexport const SANTE_VERSION = \"0.5.1\";\n"]}