france-data-mcp 0.12.2 → 0.13.0
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.
- package/README.en.md +10 -11
- package/README.md +10 -11
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +116 -7
- package/dist/index.js.map +1 -1
- package/dist/sante/index.d.ts +310 -2
- package/dist/sante/index.js +116 -7
- package/dist/sante/index.js.map +1 -1
- package/package.json +1 -2
package/README.en.md
CHANGED
|
@@ -79,21 +79,20 @@ Brings together the most useful French government data sources under a uniform t
|
|
|
79
79
|
|
|
80
80
|
## Status
|
|
81
81
|
|
|
82
|
-
✅ **v0.
|
|
82
|
+
✅ **v0.13.0 — in production.** 35 tools, 4 ingested health sources (FINESS, Ameli, RPPS, CDS) + live INSEE / DINUM / IGN. 1,257 tests, TypeScript strict. Listed on the [official MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers?search=france-data-mcp). Observability: Sentry + Axiom + `/healthz`. **68.5% of RPPS practitioners precisely geolocated** (FINESS building or BAN street) — exposed via per-result `geo_precision` + `precise_only` filter on 4 RPPS tools.
|
|
83
83
|
|
|
84
|
-
Full
|
|
84
|
+
**New in v0.13 — Resolver V2**: geographic fallback via DINUM `/near_point` filtered by NAF compatible with the FINESS family, triggered when the V0.7 RPPS pivot fails (labs with no RPPS-declared biologist, EHPADs / pharmacies without site holder, business relocations). Many-to-many NAF gate (`naf-finess-mapping.ts`) preserves the Franco-Britannique safety net (a lab is never matched to a co-located nursing school). Full traceability exposed: `method` / `fallback_reason` / `naf_filter_used` / `disambiguation_status` on `verifier_site_actif` / `historique_etablissement` / `reconcilier_finess_sirene` / `inspect_site`. Bonus: dynamic `geo_precision` label (reflects actual row distribution) + `includeDirigeants` toggle on `entreprises_in_radius` (token-saving for bulk enumeration).
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
Full history: [CHANGELOG](CHANGELOG.md). Project discipline: prove every root cause against production / official docs BEFORE coding the fix.
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
### Roadmap
|
|
89
89
|
|
|
90
|
-
- [
|
|
91
|
-
- [
|
|
92
|
-
- [
|
|
93
|
-
- [
|
|
94
|
-
- [
|
|
95
|
-
- [ ] **
|
|
96
|
-
- [ ] **v1.0+** — DOM-COM support (`code_insee CHAR(5)`), INSEE IRIS (infra-communal demographics), DPC (PS training history)
|
|
90
|
+
- [ ] **Dense-zone timeout fix**: `rpps_in_radius` with `preciseOnly=true` over dense Paris + 1 km radius triggers `57014 statement timeout`. Reproduced in production V0.13. To diagnose via `EXPLAIN ANALYZE` then fix (missing indices or planner hint).
|
|
91
|
+
- [ ] **Ameli address geocoding**: fetch precise BAN coordinates instead of commune centroid on Ameli addresses — bonus 1 from Claude.ai feedback, standalone, high impact.
|
|
92
|
+
- [ ] **Unified RPPS + Ameli professional sheet** (concatenated, not resolved — "concatenated MCP / resolved Geo Intel" doctrine): one practitioner, two sources juxtaposed, divergences exposed to the caller.
|
|
93
|
+
- [ ] **`finess_sirene_coverage_in_radius` matching fix**: IFSI/lab matching via the `naf-finess-mapping.ts` table (table is ready, to be wired into `coverage.ts`).
|
|
94
|
+
- [ ] **Phase 2 — recurring re-geocoding automation**: bounded BAN step inline in the RPPS cron (diff staging → POST BAN on delta → upsert cache). Pending Phase 1 production numbers.
|
|
95
|
+
- [ ] **v1.0+**: DOM-COM support (`code_insee CHAR(5)`), INSEE IRIS (infra-communal demographics), DPC (PS training history).
|
|
97
96
|
|
|
98
97
|
---
|
|
99
98
|
|
package/README.md
CHANGED
|
@@ -112,21 +112,20 @@ Usage intensif : throttler côté client ou self-héberger.
|
|
|
112
112
|
|
|
113
113
|
## État du projet
|
|
114
114
|
|
|
115
|
-
✅ **V0.
|
|
115
|
+
✅ **V0.13.0 — en production.** 35 tools, 4 sources santé (FINESS, Ameli, RPPS, CDS) + INSEE / DINUM / IGN. 1 257 tests, TypeScript strict. Sur le [registry MCP officiel](https://registry.modelcontextprotocol.io/v0.1/servers?search=france-data-mcp). Observabilité Sentry + Axiom + `/healthz`. **68,5 % des PS RPPS géolocalisés précisément** (FINESS bâtiment ou BAN rue) — exposé via `geo_precision` + filtre `precise_only` par-résultat sur 4 tools RPPS.
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
**Nouveau V0.13 — Resolver V2** : fallback géographique DINUM `/near_point` filtré par NAF compatible quand le pivot RPPS V0.7 échoue (labos sans biologiste RPPS, EHPAD/pharmacies sans titulaire, déménagements). Gate NAF many-to-many (`naf-finess-mapping.ts`) qui préserve le garde-fou Franco-Britannique (un labo ne sera jamais rattaché à un IFSI co-localisé). Traçabilité complète exposée : `method` / `fallback_reason` / `naf_filter_used` / `disambiguation_status` sur les 4 tools `verifier_site_actif` / `historique_etablissement` / `reconcilier_finess_sirene` / `inspect_site`. Bonus : étiquette `geo_precision` dynamique (reflète la distribution réelle des rows) + toggle `includeDirigeants` sur `entreprises_in_radius` (économie tokens en énumération volume).
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
Historique complet : [CHANGELOG](CHANGELOG.md). Discipline projet : prouver chaque cause-racine par la prod / doc officielle AVANT de coder le fix.
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
### Roadmap
|
|
122
122
|
|
|
123
|
-
- [
|
|
124
|
-
- [
|
|
125
|
-
- [
|
|
126
|
-
- [
|
|
127
|
-
- [
|
|
128
|
-
- [ ] **
|
|
129
|
-
- [ ] **V1.0+** — DOM-COM (`code_insee CHAR(5)`), INSEE IRIS (démographie infra-communale), DPC (historique formations PS)
|
|
123
|
+
- [ ] **Fix timeout zone dense** : `rpps_in_radius` avec `preciseOnly=true` sur Paris dense + 1 km déclenche `57014 statement timeout`. Confirmé en prod V0.13. À diagnostiquer via `EXPLAIN ANALYZE` puis fixer (indices manquants ou hint planner).
|
|
124
|
+
- [ ] **Géocodage Ameli adresses** : récupérer coords précises BAN au lieu du centroïde commune sur les adresses Ameli — bonus 1 du retour Claude.ai, autonome, fort impact.
|
|
125
|
+
- [ ] **Fiche professionnel unifiée RPPS + Ameli** (concaténée, pas résolue — doctrine "concaténé MCP / résolu Geo Intel") : un PS, deux sources juxtaposées, divergences exposées au caller.
|
|
126
|
+
- [ ] **Fix `finess_sirene_coverage_in_radius`** : matching IFSI/labo via la table `naf-finess-mapping.ts` (table prête, à câbler dans `coverage.ts`).
|
|
127
|
+
- [ ] **Phase 2 — automatisation re-géocodage récurrent** : brique BAN inline cron RPPS (diff staging → POST BAN delta → upsert cache). Conditionnée aux chiffres Phase 1.
|
|
128
|
+
- [ ] **V1.0+** : DOM-COM (`code_insee CHAR(5)`), INSEE IRIS (démographie infra-communale), DPC (historique formations PS).
|
|
130
129
|
|
|
131
130
|
---
|
|
132
131
|
|
package/dist/cli.js
CHANGED
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/version.ts","../bin/cli.ts"],"names":[],"mappings":";;;;;;;AASO,IAAM,OAAA,GAAU,QAAA;;;ACqBvB,IAAM,gBAAA,GAAmB,wCAAA;AACzB,IAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,mBAAA,IAAuB;AACpD,IAAM,UAAA,GAAa,uBAAuB,OAAO,CAAA;AACjD,IAAM,uBAAA,GAA0B,MAAA;AAEhC,IAAM,aAAA,GAAgB,QAAQ,GAAA,CAAI,0BAAA;AAClC,IAAM,aAAA,GAAgB,OAAO,aAAa,CAAA;AAC1C,IAAM,cAAA,GAAiB,MAAA,CAAO,QAAA,CAAS,aAAa,KAAK,aAAA,GAAgB,CAAA;AACzE,IAAM,kBAAA,GAAqB,iBAAiB,aAAA,GAAgB,GAAA;AAG5D,IAAI,aAAA,KAAkB,MAAA,IAAa,CAAC,cAAA,EAAgB;AAClD,EAAA,OAAA,CAAQ,KAAA;AAAA,IACN,CAAA,kDAAA,EAAqD,aAAa,CAAA,qBAAA,EAAwB,kBAAkB,CAAA,EAAA;AAAA,GAC9G;AACF;AASA,SAAS,QAAQ,IAAA,EAAyB;AACxC,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC3B,IAAA,IAAI,GAAA,IAAO,OAAO,GAAA,KAAQ,QAAA,IAAY,QAAQ,GAAA,EAAK;AACjD,MAAA,MAAM,KAAM,GAAA,CAAuB,EAAA;AACnC,MAAA,IAAI,OAAO,OAAO,QAAA,IAAY,OAAO,OAAO,QAAA,IAAY,EAAA,KAAO,MAAM,OAAO,EAAA;AAAA,IAC9E;AAAA,EACF,CAAA,CAAA,MAAQ;AAAA,EAIR;AACA,EAAA,OAAO,IAAA;AACT;AAEA,IAAM,eAAA,GAAkB,CAAC,CAAA,KAAoB;AAC3C,EAAA,MAAA,CAAO,MAAM,CAAC,CAAA;AAChB,CAAA;AAMA,SAAS,gBAAA,CACP,EAAA,EACA,IAAA,EACA,OAAA,EACA,QAAA,EACM;AACN,EAAA,MAAM,OAAA,GAAU,EAAE,OAAA,EAAS,KAAA,EAAO,IAAI,KAAA,EAAO,EAAE,IAAA,EAAM,OAAA,EAAQ,EAAE;AAC/D,EAAA,QAAA,CAAS,CAAA,EAAG,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC;AAAA,CAAI,CAAA;AACzC;AAQA,eAAsB,WAAA,CACpB,IAAA,EACA,OAAA,GAAwB,KAAA,EACxB,WAAgC,eAAA,EACjB;AACf,EAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAC1B,EAAA,IAAI,CAAC,OAAA,EAAS;AAGd,EAAA,MAAM,EAAA,GAAK,QAAQ,OAAO,CAAA;AAE1B,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI;AACF,IAAA,QAAA,GAAW,MAAM,QAAQ,QAAA,EAAU;AAAA,MACjC,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,YAAA,EAAc,UAAA;AAAA,QACd,MAAA,EAAQ;AAAA,OACV;AAAA,MACA,IAAA,EAAM,OAAA;AAAA,MACN,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,kBAAkB;AAAA,KAC/C,CAAA;AAAA,EACH,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,MAAA,GAAS,eAAe,GAAA,YAAe,KAAA,GAAQ,IAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA;AAC9E,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,sCAAA,EAAyC,MAAM,CAAA,CAAE,CAAA;AAC/D,IAAA,gBAAA;AAAA,MACE,EAAA;AAAA,MACA,uBAAA;AAAA,MACA,CAAA,4BAAA,EAA+B,aAAa,CAAA,EAAA,EAAK,MAAM,CAAA,CAAA;AAAA,MACvD;AAAA,KACF;AACA,IAAA;AAAA,EACF;AAEA,EAAA,IAAI,IAAA;AACJ,EAAA,IAAI;AACF,IAAA,IAAA,GAAO,MAAM,SAAS,IAAA,EAAK;AAAA,EAC7B,SAAS,GAAA,EAAK;AAKZ,IAAA,MAAM,MAAA,GAAS,eAAe,GAAA,YAAe,KAAA,GAAQ,IAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA;AAC9E,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,+CAAA,EAAkD,MAAM,CAAA,CAAE,CAAA;AACxE,IAAA,gBAAA;AAAA,MACE,EAAA;AAAA,MACA,uBAAA;AAAA,MACA,CAAA,6BAAA,EAAgC,aAAa,CAAA,EAAA,EAAK,MAAM,CAAA,CAAA;AAAA,MACxD;AAAA,KACF;AACA,IAAA;AAAA,EACF;AAEA,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,gBAAA;AAAA,MACE,EAAA;AAAA,MACA,uBAAA;AAAA,MACA,CAAA,cAAA,EAAiB,QAAA,CAAS,MAAM,CAAA,MAAA,EAAS,aAAa,CAAA,EAAA,EAAK,cAAA,CAAe,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAC,CAAC,CAAA,CAAA;AAAA,MAC7F;AAAA,KACF;AACA,IAAA;AAAA,EACF;AAKA,EAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG,QAAA,CAAS,GAAG,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,EAAE,CAAC;AAAA,CAAI,CAAA;AAChE;AAeA,SAAS,mBAAmB,QAAA,EAA0B;AACpD,EAAA,IAAI;AACF,IAAA,MAAM,CAAA,GAAI,IAAI,GAAA,CAAI,QAAQ,CAAA;AAC1B,IAAA,CAAA,CAAE,QAAA,GAAW,EAAA;AACb,IAAA,CAAA,CAAE,QAAA,GAAW,EAAA;AACb,IAAA,OAAO,EAAE,QAAA,EAAS;AAAA,EACpB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,QAAA;AAAA,EACT;AACF;AAIA,IAAM,aAAA,GAAgB,mBAAmB,QAAQ,CAAA;AAUjD,SAAS,eAAe,MAAA,EAAwB;AAC9C,EAAA,OAAO,MAAA,CAAO,OAAA,CAAQ,2BAAA,EAA6B,eAAe,CAAA;AACpE;AAEA,eAAe,IAAA,GAAsB;AACnC,EAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,uBAAA,EAA0B,OAAO,CAAA,QAAA,EAAM,aAAa,CAAA,CAAE,CAAA;AACpE,EAAA,MAAM,EAAA,GAAK,gBAAgB,EAAE,KAAA,EAAO,OAAO,SAAA,EAAW,MAAA,CAAO,mBAAmB,CAAA;AAChF,EAAA,WAAA,MAAiB,QAAQ,EAAA,EAAI;AAC3B,IAAA,MAAM,YAAY,IAAI,CAAA;AAAA,EACxB;AACF;AAcO,SAAS,YAAA,CAAa,eAAuB,KAAA,EAAoC;AACtF,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,KAAA;AACtC,EAAA,IAAI;AACF,IAAA,OAAO,aAAa,aAAA,CAAc,aAAa,CAAC,CAAA,KAAM,aAAa,KAAK,CAAA;AAAA,EAC1E,CAAA,CAAA,MAAQ;AAQN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAEA,IAAI,aAAa,MAAA,CAAA,IAAA,CAAY,GAAA,EAAK,QAAQ,IAAA,CAAK,CAAC,CAAC,CAAA,EAAG;AAClD,EAAA,IAAA,EAAK,CAAE,KAAA,CAAM,CAAC,GAAA,KAAiB;AAC7B,IAAA,MAAM,SAAS,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC9D,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,6BAAA,EAAgC,MAAM,CAAA,CAAE,CAAA;AACtD,IAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EAChB,CAAC,CAAA;AACH","file":"cli.js","sourcesContent":["/**\n * Version courante du serveur MCP + wrapper npm. Source de vérité partagée :\n * - `api/mcp.ts` → expose via `initialize.serverInfo.version` au client MCP\n * - `bin/cli.ts` → expose dans le User-Agent HTTP + le banner stderr\n *\n * Synchronisée manuellement avec `package.json.version` à chaque release.\n * Une déclaration en TS pur évite la friction des import attributes JSON\n * (instables entre tsup/esbuild/@vercel/node).\n */\nexport const VERSION = \"0.12.2\";\n","#!/usr/bin/env node\n/**\n * france-data-mcp — wrapper npm.\n *\n * Forwarde le protocole MCP stdio (NDJSON sur stdin/stdout) vers l'endpoint\n * HTTP `france-data-mcp.vercel.app/mcp`. Permet aux clients MCP qui ne savent\n * pas appeler un endpoint HTTP distant (Claude Desktop natif, certains IDE)\n * d'utiliser le serveur via `npx france-data-mcp`.\n *\n * Architecture :\n * - Lit stdin ligne par ligne (NDJSON, spec MCP stdio transport). Trim le\n * whitespace périphérique avant forward — transformation volontaire et\n * inoffensive (n'altère pas le JSON-RPC payload).\n * - Pour chaque ligne non vide, POST vers `ENDPOINT` et écrit la réponse\n * sur stdout (NDJSON).\n * - En cas d'erreur réseau, HTTP >= 400, ou body stream interrompu, émet\n * une réponse JSON-RPC error (-32603) pour ne JAMAIS faire hang le client.\n *\n * stdout doit rester pur JSON-RPC (NDJSON) — tout log interne va sur stderr\n * via `console.error` (jamais `stdout.write` pour autre chose qu'une réponse\n * JSON-RPC). Pas d'état stateful : le serveur HTTP est stateless lui aussi.\n */\n\nimport { realpathSync } from \"node:fs\";\nimport { stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { VERSION } from \"../src/core/version.js\";\n\nconst DEFAULT_ENDPOINT = \"https://france-data-mcp.vercel.app/mcp\";\nconst ENDPOINT = process.env.FRANCE_DATA_MCP_URL || DEFAULT_ENDPOINT;\nconst USER_AGENT = `france-data-mcp-npm/${VERSION}`;\nconst JSON_RPC_INTERNAL_ERROR = -32603;\n\nconst rawTimeoutEnv = process.env.FRANCE_DATA_MCP_TIMEOUT_MS;\nconst parsedTimeout = Number(rawTimeoutEnv);\nconst isValidTimeout = Number.isFinite(parsedTimeout) && parsedTimeout > 0;\nconst REQUEST_TIMEOUT_MS = isValidTimeout ? parsedTimeout : 60_000;\n// Signaler le fallback côté stderr (jamais stdout — réservé au JSON-RPC) pour\n// éviter un silent failure si l'utilisateur a tapé une valeur invalide.\nif (rawTimeoutEnv !== undefined && !isValidTimeout) {\n console.error(\n `[france-data-mcp-npm] FRANCE_DATA_MCP_TIMEOUT_MS=\"${rawTimeoutEnv}\" invalide, fallback ${REQUEST_TIMEOUT_MS}ms`,\n );\n}\n\ntype JsonRpcId = string | number | null;\ntype JsonRpcMessage = { id?: JsonRpcId; method?: string };\n\n/**\n * Extrait l'id JSON-RPC d'une ligne (best-effort). Utilisé uniquement pour\n * construire une réponse error propre quand le forward réseau échoue.\n */\nfunction parseId(line: string): JsonRpcId {\n try {\n const msg = JSON.parse(line) as unknown;\n if (msg && typeof msg === \"object\" && \"id\" in msg) {\n const id = (msg as JsonRpcMessage).id;\n if (typeof id === \"string\" || typeof id === \"number\" || id === null) return id;\n }\n } catch {\n // Best-effort : si la ligne n'est pas du JSON valide, forwardLine émettra\n // quand même une réponse JSON-RPC error sur stdout avec id=null. Le\n // diagnostic texte va sur stderr via console.error.\n }\n return null;\n}\n\nconst defaultWriteOut = (s: string): void => {\n stdout.write(s);\n};\n\n/**\n * Émet une réponse JSON-RPC error sur stdout. NDJSON appliqué de façon\n * uniforme (1 message = 1 ligne).\n */\nfunction emitJsonRpcError(\n id: JsonRpcId,\n code: number,\n message: string,\n writeOut: (s: string) => void,\n): void {\n const payload = { jsonrpc: \"2.0\", id, error: { code, message } };\n writeOut(`${JSON.stringify(payload)}\\n`);\n}\n\n/**\n * POST une ligne JSON-RPC vers l'endpoint HTTP et écrit la réponse sur stdout.\n * Catche toutes les erreurs (réseau, timeout, HTTP >=400, stream interrompu)\n * en émettant une réponse JSON-RPC error — le client MCP voit toujours une\n * réponse pour chaque request, jamais de hang.\n */\nexport async function forwardLine(\n line: string,\n fetchFn: typeof fetch = fetch,\n writeOut: (s: string) => void = defaultWriteOut,\n): Promise<void> {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n // Parsing d'id fait UNE seule fois, réutilisé sur tous les chemins d'erreur.\n const id = parseId(trimmed);\n\n let response: Response;\n try {\n response = await fetchFn(ENDPOINT, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"user-agent\": USER_AGENT,\n accept: \"application/json\",\n },\n body: trimmed,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (err) {\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] forward failed: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Network error forwarding to ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n let text: string;\n try {\n text = await response.text();\n } catch (err) {\n // Stream interrompu après les headers (gateway timeout, réseau coupé) :\n // sans ce catch, la promise rejette → main() crash → client hang sur l'id.\n // console.error pour le diagnostic local, capture Sentry inapplicable\n // côté wrapper client (par design : pas de telemetry).\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] body stream interrupted: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Body stream interrupted from ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n if (!response.ok) {\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Upstream HTTP ${response.status} from ${SAFE_ENDPOINT}: ${sanitizeReason(text.slice(0, 200))}`,\n writeOut,\n );\n return;\n }\n\n // L'endpoint Vercel renvoie un seul objet JSON-RPC (status 200) ou rien\n // (204 pour les notifications). Passthrough verbatim — pas de re-sérialisation\n // pour préserver la précision (ordre des clés, formats numériques).\n if (text.length > 0) writeOut(`${text.replace(/\\n+$/u, \"\")}\\n`);\n}\n\n/**\n * Masque les credentials userinfo d'une URL connue (notre ENDPOINT) avant\n * inclusion dans un log stderr ou un message error JSON-RPC stdout.\n *\n * Surfaces couvertes :\n * 1. Banner stderr au démarrage (`SAFE_ENDPOINT`)\n * 2. Messages error JSON-RPC qui interpolent `${SAFE_ENDPOINT}`\n * 3. Messages error des exceptions `fetch` Node 22+ — leur `err.message`\n * embarque l'URL fautive ENTIÈRE incluant userinfo (\"Request cannot be\n * constructed from a URL that includes credentials: https://user:pass@…\").\n * Cette surface est couverte par `sanitizeReason()` complémentaire — ne PAS\n * se reposer sur cette fonction seule.\n */\nfunction safeEndpointForLog(endpoint: string): string {\n try {\n const u = new URL(endpoint);\n u.username = \"\";\n u.password = \"\";\n return u.toString();\n } catch {\n return endpoint;\n }\n}\n\n// Calculé une seule fois au module load — réutilisé dans tous les messages\n// (banner stderr + 3 chemins d'erreur stdout JSON-RPC).\nconst SAFE_ENDPOINT = safeEndpointForLog(ENDPOINT);\n\n/**\n * Strip toute URL `scheme://user:pass@host` d'une string libre. Le runtime\n * `fetch` Node 22+ throw un `TypeError` dont le message contient verbatim\n * l'URL fautive, incluant userinfo. Sans sanitization, le `reason` d'une\n * erreur réseau fuiterait les credentials côté stdout (JSON-RPC error) ET\n * stderr (diagnostic). Defense-in-depth obligatoire — la même URL pourrait\n * arriver via un message d'erreur de proxy, DNS, gateway, etc.\n */\nfunction sanitizeReason(reason: string): string {\n return reason.replace(/(https?:\\/\\/)[^/\\s@]+@/giu, \"$1[redacted]@\");\n}\n\nasync function main(): Promise<void> {\n console.error(`[france-data-mcp-npm] v${VERSION} → ${SAFE_ENDPOINT}`);\n const rl = createInterface({ input: stdin, crlfDelay: Number.POSITIVE_INFINITY });\n for await (const line of rl) {\n await forwardLine(line);\n }\n}\n\n/**\n * Détecte si le module est exécuté directement (vs importé par un test).\n * Évite de démarrer la boucle stdin pendant les tests.\n *\n * V0.7.6 fix : la garde précédente comparait `pathToFileURL(argv1).href` à\n * `import.meta.url`. Quand npm/npx exécutent le bin via un symlink dans\n * `node_modules/.bin/`, `process.argv[1]` reste le chemin du symlink mais\n * Node ESM résout `import.meta.url` vers la cible réelle. Les deux divergent\n * → `main()` jamais appelé → process exit silencieux. Régression silencieuse\n * en prod depuis V0.7.2 (1er wrapper npm). Fix : comparer les `realpath` des\n * deux côtés pour matcher quel que soit le routage symlink.\n */\nexport function isMainModule(importMetaUrl: string, argv1: string | undefined): boolean {\n if (typeof argv1 !== \"string\") return false;\n try {\n return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(argv1);\n } catch {\n // Catch volontairement silencieux (PAS un silent failure métier) :\n // c'est une décision booléenne sans effet utilisateur — on ne hand off\n // aucune donnée, on ne masque aucune erreur API. Si realpath ou\n // fileURLToPath throw (URL malformée, fichier inexistant — typique en\n // contexte test/import abstrait), la bonne réponse est \"ne PAS démarrer\n // main()\", pas \"logger une erreur\" qui polluerait stderr de tous les\n // tests qui importent le module.\n return false;\n }\n}\n\nif (isMainModule(import.meta.url, process.argv[1])) {\n main().catch((err: unknown) => {\n const reason = err instanceof Error ? err.message : String(err);\n console.error(`[france-data-mcp-npm] fatal: ${reason}`);\n process.exit(1);\n });\n}\n\nexport { ENDPOINT, USER_AGENT, parseId, safeEndpointForLog };\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/core/version.ts","../bin/cli.ts"],"names":[],"mappings":"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","file":"cli.js","sourcesContent":["/**\n * Version courante du serveur MCP + wrapper npm. Source de vérité partagée :\n * - `api/mcp.ts` → expose via `initialize.serverInfo.version` au client MCP\n * - `bin/cli.ts` → expose dans le User-Agent HTTP + le banner stderr\n *\n * Synchronisée manuellement avec `package.json.version` à chaque release.\n * Une déclaration en TS pur évite la friction des import attributes JSON\n * (instables entre tsup/esbuild/@vercel/node).\n */\nexport const VERSION = \"0.13.0\";\n","#!/usr/bin/env node\n/**\n * france-data-mcp — wrapper npm.\n *\n * Forwarde le protocole MCP stdio (NDJSON sur stdin/stdout) vers l'endpoint\n * HTTP `france-data-mcp.vercel.app/mcp`. Permet aux clients MCP qui ne savent\n * pas appeler un endpoint HTTP distant (Claude Desktop natif, certains IDE)\n * d'utiliser le serveur via `npx france-data-mcp`.\n *\n * Architecture :\n * - Lit stdin ligne par ligne (NDJSON, spec MCP stdio transport). Trim le\n * whitespace périphérique avant forward — transformation volontaire et\n * inoffensive (n'altère pas le JSON-RPC payload).\n * - Pour chaque ligne non vide, POST vers `ENDPOINT` et écrit la réponse\n * sur stdout (NDJSON).\n * - En cas d'erreur réseau, HTTP >= 400, ou body stream interrompu, émet\n * une réponse JSON-RPC error (-32603) pour ne JAMAIS faire hang le client.\n *\n * stdout doit rester pur JSON-RPC (NDJSON) — tout log interne va sur stderr\n * via `console.error` (jamais `stdout.write` pour autre chose qu'une réponse\n * JSON-RPC). Pas d'état stateful : le serveur HTTP est stateless lui aussi.\n */\n\nimport { realpathSync } from \"node:fs\";\nimport { stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { VERSION } from \"../src/core/version.js\";\n\nconst DEFAULT_ENDPOINT = \"https://france-data-mcp.vercel.app/mcp\";\nconst ENDPOINT = process.env.FRANCE_DATA_MCP_URL || DEFAULT_ENDPOINT;\nconst USER_AGENT = `france-data-mcp-npm/${VERSION}`;\nconst JSON_RPC_INTERNAL_ERROR = -32603;\n\nconst rawTimeoutEnv = process.env.FRANCE_DATA_MCP_TIMEOUT_MS;\nconst parsedTimeout = Number(rawTimeoutEnv);\nconst isValidTimeout = Number.isFinite(parsedTimeout) && parsedTimeout > 0;\nconst REQUEST_TIMEOUT_MS = isValidTimeout ? parsedTimeout : 60_000;\n// Signaler le fallback côté stderr (jamais stdout — réservé au JSON-RPC) pour\n// éviter un silent failure si l'utilisateur a tapé une valeur invalide.\nif (rawTimeoutEnv !== undefined && !isValidTimeout) {\n console.error(\n `[france-data-mcp-npm] FRANCE_DATA_MCP_TIMEOUT_MS=\"${rawTimeoutEnv}\" invalide, fallback ${REQUEST_TIMEOUT_MS}ms`,\n );\n}\n\ntype JsonRpcId = string | number | null;\ntype JsonRpcMessage = { id?: JsonRpcId; method?: string };\n\n/**\n * Extrait l'id JSON-RPC d'une ligne (best-effort). Utilisé uniquement pour\n * construire une réponse error propre quand le forward réseau échoue.\n */\nfunction parseId(line: string): JsonRpcId {\n try {\n const msg = JSON.parse(line) as unknown;\n if (msg && typeof msg === \"object\" && \"id\" in msg) {\n const id = (msg as JsonRpcMessage).id;\n if (typeof id === \"string\" || typeof id === \"number\" || id === null) return id;\n }\n } catch {\n // Best-effort : si la ligne n'est pas du JSON valide, forwardLine émettra\n // quand même une réponse JSON-RPC error sur stdout avec id=null. Le\n // diagnostic texte va sur stderr via console.error.\n }\n return null;\n}\n\nconst defaultWriteOut = (s: string): void => {\n stdout.write(s);\n};\n\n/**\n * Émet une réponse JSON-RPC error sur stdout. NDJSON appliqué de façon\n * uniforme (1 message = 1 ligne).\n */\nfunction emitJsonRpcError(\n id: JsonRpcId,\n code: number,\n message: string,\n writeOut: (s: string) => void,\n): void {\n const payload = { jsonrpc: \"2.0\", id, error: { code, message } };\n writeOut(`${JSON.stringify(payload)}\\n`);\n}\n\n/**\n * POST une ligne JSON-RPC vers l'endpoint HTTP et écrit la réponse sur stdout.\n * Catche toutes les erreurs (réseau, timeout, HTTP >=400, stream interrompu)\n * en émettant une réponse JSON-RPC error — le client MCP voit toujours une\n * réponse pour chaque request, jamais de hang.\n */\nexport async function forwardLine(\n line: string,\n fetchFn: typeof fetch = fetch,\n writeOut: (s: string) => void = defaultWriteOut,\n): Promise<void> {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n // Parsing d'id fait UNE seule fois, réutilisé sur tous les chemins d'erreur.\n const id = parseId(trimmed);\n\n let response: Response;\n try {\n response = await fetchFn(ENDPOINT, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"user-agent\": USER_AGENT,\n accept: \"application/json\",\n },\n body: trimmed,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (err) {\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] forward failed: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Network error forwarding to ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n let text: string;\n try {\n text = await response.text();\n } catch (err) {\n // Stream interrompu après les headers (gateway timeout, réseau coupé) :\n // sans ce catch, la promise rejette → main() crash → client hang sur l'id.\n // console.error pour le diagnostic local, capture Sentry inapplicable\n // côté wrapper client (par design : pas de telemetry).\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] body stream interrupted: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Body stream interrupted from ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n if (!response.ok) {\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Upstream HTTP ${response.status} from ${SAFE_ENDPOINT}: ${sanitizeReason(text.slice(0, 200))}`,\n writeOut,\n );\n return;\n }\n\n // L'endpoint Vercel renvoie un seul objet JSON-RPC (status 200) ou rien\n // (204 pour les notifications). Passthrough verbatim — pas de re-sérialisation\n // pour préserver la précision (ordre des clés, formats numériques).\n if (text.length > 0) writeOut(`${text.replace(/\\n+$/u, \"\")}\\n`);\n}\n\n/**\n * Masque les credentials userinfo d'une URL connue (notre ENDPOINT) avant\n * inclusion dans un log stderr ou un message error JSON-RPC stdout.\n *\n * Surfaces couvertes :\n * 1. Banner stderr au démarrage (`SAFE_ENDPOINT`)\n * 2. Messages error JSON-RPC qui interpolent `${SAFE_ENDPOINT}`\n * 3. Messages error des exceptions `fetch` Node 22+ — leur `err.message`\n * embarque l'URL fautive ENTIÈRE incluant userinfo (\"Request cannot be\n * constructed from a URL that includes credentials: https://user:pass@…\").\n * Cette surface est couverte par `sanitizeReason()` complémentaire — ne PAS\n * se reposer sur cette fonction seule.\n */\nfunction safeEndpointForLog(endpoint: string): string {\n try {\n const u = new URL(endpoint);\n u.username = \"\";\n u.password = \"\";\n return u.toString();\n } catch {\n return endpoint;\n }\n}\n\n// Calculé une seule fois au module load — réutilisé dans tous les messages\n// (banner stderr + 3 chemins d'erreur stdout JSON-RPC).\nconst SAFE_ENDPOINT = safeEndpointForLog(ENDPOINT);\n\n/**\n * Strip toute URL `scheme://user:pass@host` d'une string libre. Le runtime\n * `fetch` Node 22+ throw un `TypeError` dont le message contient verbatim\n * l'URL fautive, incluant userinfo. Sans sanitization, le `reason` d'une\n * erreur réseau fuiterait les credentials côté stdout (JSON-RPC error) ET\n * stderr (diagnostic). Defense-in-depth obligatoire — la même URL pourrait\n * arriver via un message d'erreur de proxy, DNS, gateway, etc.\n */\nfunction sanitizeReason(reason: string): string {\n return reason.replace(/(https?:\\/\\/)[^/\\s@]+@/giu, \"$1[redacted]@\");\n}\n\nasync function main(): Promise<void> {\n console.error(`[france-data-mcp-npm] v${VERSION} → ${SAFE_ENDPOINT}`);\n const rl = createInterface({ input: stdin, crlfDelay: Number.POSITIVE_INFINITY });\n for await (const line of rl) {\n await forwardLine(line);\n }\n}\n\n/**\n * Détecte si le module est exécuté directement (vs importé par un test).\n * Évite de démarrer la boucle stdin pendant les tests.\n *\n * V0.7.6 fix : la garde précédente comparait `pathToFileURL(argv1).href` à\n * `import.meta.url`. Quand npm/npx exécutent le bin via un symlink dans\n * `node_modules/.bin/`, `process.argv[1]` reste le chemin du symlink mais\n * Node ESM résout `import.meta.url` vers la cible réelle. Les deux divergent\n * → `main()` jamais appelé → process exit silencieux. Régression silencieuse\n * en prod depuis V0.7.2 (1er wrapper npm). Fix : comparer les `realpath` des\n * deux côtés pour matcher quel que soit le routage symlink.\n */\nexport function isMainModule(importMetaUrl: string, argv1: string | undefined): boolean {\n if (typeof argv1 !== \"string\") return false;\n try {\n return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(argv1);\n } catch {\n // Catch volontairement silencieux (PAS un silent failure métier) :\n // c'est une décision booléenne sans effet utilisateur — on ne hand off\n // aucune donnée, on ne masque aucune erreur API. Si realpath ou\n // fileURLToPath throw (URL malformée, fichier inexistant — typique en\n // contexte test/import abstrait), la bonne réponse est \"ne PAS démarrer\n // main()\", pas \"logger une erreur\" qui polluerait stderr de tous les\n // tests qui importent le module.\n return false;\n }\n}\n\nif (isMainModule(import.meta.url, process.argv[1])) {\n main().catch((err: unknown) => {\n const reason = err instanceof Error ? err.message : String(err);\n console.error(`[france-data-mcp-npm] fatal: ${reason}`);\n process.exit(1);\n });\n}\n\nexport { ENDPOINT, USER_AGENT, parseId, safeEndpointForLog };\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { Commune, GeoLevel, GeocodeOptions, GeocodeResult, PopulationData, SearchCommunesOptions, TERRITOIRE_VERSION, geocode, geocodeMany, getCommuneByCode, getPopulationByCommune, getPopulationByDept, reverseGeocode, searchCommunes } from './territoire/index.js';
|
|
2
|
-
export { AnsFhirPractitioner, ByCategorieInput, Dirigeant, Entreprise, Etablissement, EtablissementFiness, FINESS_CATEGORIES, FINESS_EHPAD, FINESS_FAMILY_CODES, FINESS_HOPITAUX, FINESS_LABOS, FINESS_MSP_CPTS, FINESS_PHARMACIES, FilterAnnuaireOptions, Finance, FinessCategorieCode, FinessFamille, FinessFamilleQuery, FinessQueryResult, FinessResult, InRadiusInput, LoadFinessOptions, NAF_EHPAD, NAF_LABOS, NAF_MEDECINE_VILLE, NAF_PHARMACIES, NAF_SANTE, NafCodeSante, ProfessionnelSante, RPPS_CGU_NOTICE, RPPS_MODE_EXERCICE, RppsDansEtablissementInput, RppsInRadiusInput, RppsLookupResult, RppsParSpecialiteDeptInput, RppsQueryResult, RppsResult, SANTE_VERSION, SearchEntreprisesOptions, SearchEntreprisesResult, SearchFinessOptions, StreamAnnuaireOptions, ensureAnnuaireAmeli, finessFamille, getAnsFhirApiKey, getAnsFhirBaseUrl, getEntrepriseBySiren, getFinessByCategorie, getFinessByNumFiness, getFinessInRadius, getInseeApiKey, getRppsById, getRppsDansEtablissement, getRppsInRadius, getRppsParSpecialiteDept, haversineDistance, libelleCategorieFiness, libelleNaf, loadFiness, loadProfessionnels, lookupPractitionerByRpps, lookupSirenViaInsee, searchEntreprises, searchEtablissementsFiness, streamProfessionnels } from './sante/index.js';
|
|
2
|
+
export { AnsFhirPractitioner, ByCategorieInput, DELIBERATELY_NO_NAF, DinumLookupError, Dirigeant, DisambiguationStatus, Entreprise, Etablissement, EtablissementFiness, FINESS_CATEGORIES, FINESS_EHPAD, FINESS_FAMILY_CODES, FINESS_HOPITAUX, FINESS_LABOS, FINESS_MSP_CPTS, FINESS_PHARMACIES, FallbackReason, FilterAnnuaireOptions, Finance, FinessCategorieCode, FinessFamille, FinessFamilleQuery, FinessQueryResult, FinessResult, InRadiusInput, LoadFinessOptions, NAF_EHPAD, NAF_LABOS, NAF_MEDECINE_VILLE, NAF_PHARMACIES, NAF_SANTE, NafCodeSante, ProfessionnelSante, RPPS_CGU_NOTICE, RPPS_MODE_EXERCICE, ResolutionMethod, RppsDansEtablissementInput, RppsInRadiusInput, RppsLookupResult, RppsParSpecialiteDeptInput, RppsQueryResult, RppsResult, SANTE_VERSION, SearchEntreprisesOptions, SearchEntreprisesResult, SearchFinessOptions, SiretCandidate, SiretCandidateSource, SiretResolution, StreamAnnuaireOptions, ensureAnnuaireAmeli, finessFamille, getAnsFhirApiKey, getAnsFhirBaseUrl, getEntrepriseBySiren, getFinessByCategorie, getFinessByNumFiness, getFinessInRadius, getInseeApiKey, getRppsById, getRppsDansEtablissement, getRppsInRadius, getRppsParSpecialiteDept, haversineDistance, isNafCompatibleWithFamille, libelleCategorieFiness, libelleNaf, loadFiness, loadProfessionnels, lookupPractitionerByRpps, lookupSirenViaInsee, nafsForFamille, searchEntreprises, searchEtablissementsFiness, streamProfessionnels } from './sante/index.js';
|
|
3
3
|
export { C as Coordinates, R as RateLimitOptions } from './types-6cvLQmuz.js';
|
package/dist/index.js
CHANGED
|
@@ -1401,6 +1401,70 @@ function libelleNaf(code) {
|
|
|
1401
1401
|
return NAF_SANTE[code];
|
|
1402
1402
|
}
|
|
1403
1403
|
|
|
1404
|
+
// src/sante/naf-finess-mapping.ts
|
|
1405
|
+
var NAF_BY_FAMILLE = {
|
|
1406
|
+
// ─── Sanitaire ────────────────────────────────────────────────────────
|
|
1407
|
+
mco: ["8610Z"],
|
|
1408
|
+
ssr: ["8610Z"],
|
|
1409
|
+
sld: ["8610Z"],
|
|
1410
|
+
had: ["8610Z"],
|
|
1411
|
+
psychiatrie: ["8610Z", "8720A"],
|
|
1412
|
+
dialyse: ["8610Z", "8690F"],
|
|
1413
|
+
ambulatoire: ["8621Z", "8622A", "8622B", "8622C", "8623Z", "8690F"],
|
|
1414
|
+
// ─── Bio / pharma / imagerie ──────────────────────────────────────────
|
|
1415
|
+
labo: ["8690B"],
|
|
1416
|
+
// Imagerie : 8622A pour les cabinets de radiologie libéraux (radiodiagnostic
|
|
1417
|
+
// et radiothérapie au sens INSEE), 8690F pour les centres montés en société
|
|
1418
|
+
// de moyens (SCM/SEL) classés dans le fourre-tout santé humaine n.c.a. —
|
|
1419
|
+
// sans ce filet, on raterait une part importante des centres d'imagerie en
|
|
1420
|
+
// fallback (ajustement Cyril 2026-05-21).
|
|
1421
|
+
imagerie: ["8622A", "8690F"],
|
|
1422
|
+
pharmacie: ["4773Z"],
|
|
1423
|
+
// ─── Pluri-pro ────────────────────────────────────────────────────────
|
|
1424
|
+
msp_cpts: ["8621Z", "8622C", "8690F"],
|
|
1425
|
+
// ─── Personnes âgées ──────────────────────────────────────────────────
|
|
1426
|
+
ehpad: ["8710A", "8730A"],
|
|
1427
|
+
residence_autonomie: ["8730A", "8710A"],
|
|
1428
|
+
senior_accompagnement: ["8810B"],
|
|
1429
|
+
// ─── Domicile ─────────────────────────────────────────────────────────
|
|
1430
|
+
ssiad: ["8690D", "8810A"],
|
|
1431
|
+
aide_domicile: ["8810A", "8690D"],
|
|
1432
|
+
// ─── Handicap ─────────────────────────────────────────────────────────
|
|
1433
|
+
handicap_enfants: ["8710B", "8891B", "8899A"],
|
|
1434
|
+
handicap_adultes: ["8710C", "8720A", "8730B", "8810B", "8810C"],
|
|
1435
|
+
// ─── Addictologie / précarité sanitaire ───────────────────────────────
|
|
1436
|
+
addictologie: ["8610Z", "8720B", "8690F"],
|
|
1437
|
+
// ─── Enfance / protection ─────────────────────────────────────────────
|
|
1438
|
+
enfance_protection: ["8891A", "8899A"],
|
|
1439
|
+
// ─── PMI / petite enfance / santé scolaire ────────────────────────────
|
|
1440
|
+
pmi: ["8621Z", "8690F"],
|
|
1441
|
+
// ─── Hébergement social ───────────────────────────────────────────────
|
|
1442
|
+
hebergement_social: ["8720A", "8720B", "8730A", "8730B", "8899B"],
|
|
1443
|
+
// ─── Prévention / santé publique ──────────────────────────────────────
|
|
1444
|
+
prevention_sante: ["8610Z", "8690F"],
|
|
1445
|
+
// ─── Groupements de coopération ───────────────────────────────────────
|
|
1446
|
+
// ⚠️ AUCUN NAF : un GCS/GCSMS est une structure juridique transverse, pas
|
|
1447
|
+
// une activité économique propre. Listé dans DELIBERATELY_NO_NAF — le
|
|
1448
|
+
// fallback géo est désactivé pour cette famille (skip silencieux).
|
|
1449
|
+
// CI invariant `groupement is in DELIBERATELY_NO_NAF`.
|
|
1450
|
+
groupement: []
|
|
1451
|
+
};
|
|
1452
|
+
var DELIBERATELY_NO_NAF = /* @__PURE__ */ new Set([
|
|
1453
|
+
"groupement"
|
|
1454
|
+
]);
|
|
1455
|
+
function nafsForFamille(famille) {
|
|
1456
|
+
if (famille === "autre") return [];
|
|
1457
|
+
if (DELIBERATELY_NO_NAF.has(famille)) return [];
|
|
1458
|
+
return NAF_BY_FAMILLE[famille] ?? [];
|
|
1459
|
+
}
|
|
1460
|
+
function isNafCompatibleWithFamille(naf, famille) {
|
|
1461
|
+
if (!naf) return false;
|
|
1462
|
+
const compatibles = nafsForFamille(famille);
|
|
1463
|
+
if (compatibles.length === 0) return false;
|
|
1464
|
+
const normalized = naf.replace(/\./g, "").toUpperCase().trim();
|
|
1465
|
+
return compatibles.includes(normalized);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1404
1468
|
// src/core/query-metadata.ts
|
|
1405
1469
|
var SOURCE_NOTE = {
|
|
1406
1470
|
centroide_commune_ameli: "Coordonn\xE9es Ameli = centro\xEFde commune (~3 km moyenne). Adapt\xE9 \xE0 l'analyse de densit\xE9 m\xE9dicale, pas au g\xE9ocodage adresse.",
|
|
@@ -1408,21 +1472,29 @@ var SOURCE_NOTE = {
|
|
|
1408
1472
|
/** @deprecated V0.12.0 — Plus aucune RPC RPPS ne produit cet alias ; toutes ont migré vers `centroide_commune_ans_mixte` (précision hybride par-résultat). Conservé pour rétrocompat de tout client qui aurait caché la string `geo_precision` côté query_metadata (Claude.ai, Cursor, agents loggant). Ne pas réintroduire dans un nouveau call site. */
|
|
1409
1473
|
centroide_commune_ans: "Coordonn\xE9es RPPS/ANS = centro\xEFde commune (~3 km moyenne). Source : Annuaire Sant\xE9 ANS \u2014 Licence Ouverte v2.0. Pour une pr\xE9cision adresse, croiser num_finess avec etablissement_by_finess.",
|
|
1410
1474
|
centroide_commune_ans_mixte: 'Coordonn\xE9es RPPS HYBRIDES (V0.12.0) : la pr\xE9cision est MIXTE par r\xE9sultat \u2014 lire `geo_precision` PAR PS. ~68,5 % sont pr\xE9cis (`"adresse"` BAN rue/b\xE2timent ou `"etablissement_finess"` site FINESS joint via num_finess) avec `distance_km` exacte ; ~31,5 % restent au centro\xEFde commune (`"centroide_commune"`, ~3 km, `distance_km` non discriminante intra-commune). Source : Annuaire Sant\xE9 ANS \u2014 Licence Ouverte v2.0. Pour FORCER 100 % de r\xE9sultats pr\xE9cis (rayons courts <3 km, classement individuel), passer `precise_only: true` c\xF4t\xE9 tool radius.',
|
|
1475
|
+
centroide_commune_ans_precis_uniquement: "Coordonn\xE9es RPPS \u2014 variante effective V0.13.0 : TOUS les r\xE9sultats retourn\xE9s sur cette requ\xEAte sont en pr\xE9cision exacte (`adresse` BAN ou `etablissement_finess`). `distance_km` est exacte au m\xE8tre pr\xE8s pour chaque PS retourn\xE9, classement individuel fiable. Source : Annuaire Sant\xE9 ANS \u2014 Licence Ouverte v2.0. NB : la donn\xE9e source reste hybride (mixte) \u2014 d'autres PS au centro\xEFde commune existent peut-\xEAtre dans la zone mais \xE9taient hors rayon ou filtr\xE9s par `precise_only: true`.",
|
|
1476
|
+
centroide_commune_ans_centroide_uniquement: "Coordonn\xE9es RPPS \u2014 variante effective V0.13.0 : TOUS les r\xE9sultats retourn\xE9s sur cette requ\xEAte sont au centro\xEFde commune (~3 km). `distance_km` n'est PAS discriminante intra-commune (tous les PS d'une m\xEAme commune ont la m\xEAme distance au centre du rayon). Source : Annuaire Sant\xE9 ANS \u2014 Licence Ouverte v2.0. Pour un classement fiable, pivoter via FINESS (etablissement_by_finess) ou \xE9largir radius_km au-del\xE0 de ~3 km pour capter aussi les PS pr\xE9cis.",
|
|
1411
1477
|
structure_finess: "Liste rattach\xE9e \xE0 un FINESS site. Le mode_exercice r\xE9v\xE8le la nature du lien (lib\xE9ral / salari\xE9). Couverture RPPS quand le PS l'a d\xE9clar\xE9 ; salari\xE9s CH/CHU/cliniques bien couverts.",
|
|
1412
1478
|
centroide_commune_cds: "Coordonn\xE9es CDS = centro\xEFde commune (~3 km moyenne) \u2014 pas de coords natives dans le CSV CNAM. Source : Annuaire sant\xE9 Ameli, Assurance Maladie (mention obligatoire L.1461-2 CSP). Pivot via etab_finess vers FINESS DREES pour pr\xE9cision adresse."
|
|
1413
1479
|
};
|
|
1414
1480
|
var HAVERSINE_NOTE = "Distance calcul\xE9e en vol d'oiseau (haversine PostGIS). Pour la distance routi\xE8re, croiser avec un service externe (OSRM, ORS).";
|
|
1415
1481
|
var CENTROIDE_COMMUNE_RESOLUTION_KM = 3;
|
|
1416
|
-
var CENTROID_PRECISIONS =
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1482
|
+
var CENTROID_PRECISIONS = {
|
|
1483
|
+
lambert93_natif_finess: false,
|
|
1484
|
+
centroide_commune_ameli: true,
|
|
1485
|
+
centroide_commune_ans: true,
|
|
1486
|
+
// deprecated, plus produit (cf. SOURCE_NOTE)
|
|
1487
|
+
centroide_commune_ans_mixte: false,
|
|
1488
|
+
centroide_commune_ans_precis_uniquement: false,
|
|
1489
|
+
centroide_commune_ans_centroide_uniquement: true,
|
|
1490
|
+
centroide_commune_cds: true,
|
|
1491
|
+
structure_finess: false
|
|
1492
|
+
};
|
|
1421
1493
|
function isShortRadius(radiusKm) {
|
|
1422
1494
|
return radiusKm !== void 0 && radiusKm < CENTROIDE_COMMUNE_RESOLUTION_KM;
|
|
1423
1495
|
}
|
|
1424
1496
|
function isSubCommuneRadius(precision, radiusKm) {
|
|
1425
|
-
return isShortRadius(radiusKm) && CENTROID_PRECISIONS
|
|
1497
|
+
return isShortRadius(radiusKm) && CENTROID_PRECISIONS[precision];
|
|
1426
1498
|
}
|
|
1427
1499
|
var subCommuneRadiusNote = (radiusKm) => `radius_km=${radiusKm} < ${CENTROIDE_COMMUNE_RESOLUTION_KM} km : incompatible avec une pr\xE9cision au centro\xEFde commune. Le filtre rayon s'applique au centro\xEFde unique de chaque commune, pas aux adresses r\xE9elles \u2014 TOUS les PS d'une commune sont inclus ou exclus en bloc, et \`distance_km\` ne discrimine pas les PS d'une m\xEAme commune. Un r\xE9sultat vide peut \xEAtre un FAUX n\xE9gatif (centro\xEFde hors rayon), pas un d\xE9sert m\xE9dical. Pour une vraie g\xE9olocalisation adresse, pivoter via FINESS (etablissement_by_finess) ou \xE9largir radius_km \u2265 ${CENTROIDE_COMMUNE_RESOLUTION_KM}.`;
|
|
1428
1500
|
function buildMetadata(precision, withDistance, radiusKm) {
|
|
@@ -1439,6 +1511,40 @@ function buildMetadata(precision, withDistance, radiusKm) {
|
|
|
1439
1511
|
}
|
|
1440
1512
|
var finessRadiusMetadata = () => buildMetadata("lambert93_natif_finess", true);
|
|
1441
1513
|
var finessByCategorieMetadata = () => buildMetadata("lambert93_natif_finess", false);
|
|
1514
|
+
function refineRppsGeoPrecisionLabel(rows, baseMeta) {
|
|
1515
|
+
if (baseMeta.geo_precision !== "centroide_commune_ans_mixte") return baseMeta;
|
|
1516
|
+
if (rows.length === 0) return baseMeta;
|
|
1517
|
+
let precisCount = 0;
|
|
1518
|
+
let centroideCount = 0;
|
|
1519
|
+
for (const row of rows) {
|
|
1520
|
+
const p = row.geo_precision;
|
|
1521
|
+
if (p === "adresse" || p === "etablissement_finess") precisCount++;
|
|
1522
|
+
else if (p === "centroide_commune") centroideCount++;
|
|
1523
|
+
else {
|
|
1524
|
+
console.warn(
|
|
1525
|
+
`[france-data-mcp] refineRppsGeoPrecisionLabel: row sans geo_precision typ\xE9 (valeur=${JSON.stringify(p)}) \u2014 \xE9tiquette mixte pr\xE9serv\xE9e par s\xE9curit\xE9, drift RPC suspect\xE9e si coords non-null.`
|
|
1526
|
+
);
|
|
1527
|
+
return baseMeta;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
let refinedPrecision;
|
|
1531
|
+
if (precisCount === rows.length) {
|
|
1532
|
+
refinedPrecision = "centroide_commune_ans_precis_uniquement";
|
|
1533
|
+
} else if (centroideCount === rows.length) {
|
|
1534
|
+
refinedPrecision = "centroide_commune_ans_centroide_uniquement";
|
|
1535
|
+
} else {
|
|
1536
|
+
return baseMeta;
|
|
1537
|
+
}
|
|
1538
|
+
let trailingNotes = baseMeta.notes.slice(1);
|
|
1539
|
+
if (refinedPrecision === "centroide_commune_ans_centroide_uniquement") {
|
|
1540
|
+
trailingNotes = trailingNotes.filter((n) => !n.includes("La branche pr\xE9cise"));
|
|
1541
|
+
}
|
|
1542
|
+
return {
|
|
1543
|
+
...baseMeta,
|
|
1544
|
+
geo_precision: refinedPrecision,
|
|
1545
|
+
notes: [SOURCE_NOTE[refinedPrecision], ...trailingNotes]
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1442
1548
|
var rppsRadiusMetadata = (radiusKm) => {
|
|
1443
1549
|
const md = buildMetadata("centroide_commune_ans_mixte", true, radiusKm);
|
|
1444
1550
|
if (isShortRadius(radiusKm)) {
|
|
@@ -1727,6 +1833,9 @@ async function getRppsInRadius(input) {
|
|
|
1727
1833
|
limit,
|
|
1728
1834
|
rppsRadiusMetadata(input.radiusKm)
|
|
1729
1835
|
);
|
|
1836
|
+
if (result.query_metadata) {
|
|
1837
|
+
result.query_metadata = refineRppsGeoPrecisionLabel(result.results, result.query_metadata);
|
|
1838
|
+
}
|
|
1730
1839
|
if (input.preciseOnly === true && result.count === 0 && result.query_metadata) {
|
|
1731
1840
|
result.query_metadata.notes.push(
|
|
1732
1841
|
"precise_only=true et 0 r\xE9sultat dans le rayon : il peut exister des PS au centro\xEFde commune dans la zone (geom_source='commune_centroid' exclus de la branche pr\xE9cise). Relancer avec precise_only=false (mode hybride) pour les inclure, ou \xE9largir radius_km."
|
|
@@ -2003,6 +2112,6 @@ function mapPractitioner(resource, expectedRpps) {
|
|
|
2003
2112
|
// src/sante/index.ts
|
|
2004
2113
|
var SANTE_VERSION = "0.5.1";
|
|
2005
2114
|
|
|
2006
|
-
export { FINESS_CATEGORIES, FINESS_EHPAD, FINESS_FAMILY_CODES, FINESS_HOPITAUX, FINESS_LABOS, FINESS_MSP_CPTS, FINESS_PHARMACIES, NAF_EHPAD, NAF_LABOS, NAF_MEDECINE_VILLE, NAF_PHARMACIES, NAF_SANTE, RPPS_CGU_NOTICE, RPPS_MODE_EXERCICE, SANTE_VERSION, TERRITOIRE_VERSION, ensureAnnuaireAmeli, finessFamille, geocode, geocodeMany, getAnsFhirApiKey, getAnsFhirBaseUrl, getCommuneByCode, getEntrepriseBySiren, getFinessByCategorie, getFinessByNumFiness, getFinessInRadius, getInseeApiKey, getPopulationByCommune, getPopulationByDept, getRppsById, getRppsDansEtablissement, getRppsInRadius, getRppsParSpecialiteDept, haversineDistance, libelleCategorieFiness, libelleNaf, loadFiness, loadProfessionnels, lookupPractitionerByRpps, lookupSirenViaInsee, reverseGeocode, searchCommunes, searchEntreprises, searchEtablissementsFiness, streamProfessionnels };
|
|
2115
|
+
export { DELIBERATELY_NO_NAF, FINESS_CATEGORIES, FINESS_EHPAD, FINESS_FAMILY_CODES, FINESS_HOPITAUX, FINESS_LABOS, FINESS_MSP_CPTS, FINESS_PHARMACIES, NAF_EHPAD, NAF_LABOS, NAF_MEDECINE_VILLE, NAF_PHARMACIES, NAF_SANTE, RPPS_CGU_NOTICE, RPPS_MODE_EXERCICE, SANTE_VERSION, TERRITOIRE_VERSION, ensureAnnuaireAmeli, finessFamille, geocode, geocodeMany, getAnsFhirApiKey, getAnsFhirBaseUrl, getCommuneByCode, getEntrepriseBySiren, getFinessByCategorie, getFinessByNumFiness, getFinessInRadius, getInseeApiKey, getPopulationByCommune, getPopulationByDept, getRppsById, getRppsDansEtablissement, getRppsInRadius, getRppsParSpecialiteDept, haversineDistance, isNafCompatibleWithFamille, libelleCategorieFiness, libelleNaf, loadFiness, loadProfessionnels, lookupPractitionerByRpps, lookupSirenViaInsee, nafsForFamille, reverseGeocode, searchCommunes, searchEntreprises, searchEtablissementsFiness, streamProfessionnels };
|
|
2007
2116
|
//# sourceMappingURL=index.js.map
|
|
2008
2117
|
//# sourceMappingURL=index.js.map
|