@beparallel/grouping
v1.0.3
Published
Node.js wrapper for the official ATIH grouping engine (PMSI GHM/GHS) used in French hospital medical coding. This package provides access to the native C groupage module for classifying inpatient stays into GHM and GHS codes as part of the PMSI/T2A system
Downloads
1,198
Readme
grouping
A Node.js wrapper for the official ATIH grouping engine used in the French PMSI (Programme de Médicalisation des Systèmes d'Information).
This package allows integration of the native C-based groupage module to classify hospital stays into GHM (Groupe Homogène de Malades) and GHS (Groupe Homogène de Séjours) codes, in compliance with the T2A billing system in France.
Overview
This project implements a Node.js native addon using Node-API to wrap the official ATIH (Agence Technique de l'Information sur l'Hospitalisation) grouping engine. The grouping engine is a critical component of the French healthcare system's billing and classification infrastructure.
French Medical Coding Context
The French healthcare system uses the PMSI (Programme de Médicalisation des Systèmes d'Information) for medical activity coding and hospital billing under the T2A (Tarification À l'Activité) system. This project specifically addresses the need for automated grouping of medical stays into standardized categories.
For comprehensive background on grouping algorithms and medical coding in France, refer to this detailed overview.
Technical Architecture
This package consists of:
- Native C/C++ Module: The core ATIH grouping engine in the
atih/folder (see doc/atih/grouping_method.md) - JavaScript Interface: High-level Node.js API in
src/that converts JS objects to ATIH RSS strings and parses the native result - Type Definitions: TypeScript definitions for development support
- Runtime data: ATIH reference tables shipped with the package under
tables/V2024,tables/V2025,tables/V2026
Repository layout
| Path | Purpose |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| atih/ | Native C sources for the ATIH grouping engine (grouping/) and the FICUM control module (ficum/) |
| src/ | TypeScript API: exports/ (public functions + types), utils/ (RUM model, 022/023 format serializers, enums) |
| tables/V{2024,2025,2026}/ | ATIH reference tables loaded at runtime by year |
| lib/, txt/ | Supporting ATIH data files |
| doc/ | ATIH format specs and the publish runbook |
| tools/grouping-fixtures/ | Tooling to capture deterministic test fixtures from the AlphaCode API (see its README) |
| src/tests/fixtures/ | Captured .case.json fixtures asserted by the grouping test suite |
The native engine loads its tables relative to the package root (resolved via
require.resolve('@beparallel/grouping/package.json')), not the consuming app's working directory. Thetables/,lib/, andtxt/folders are part of the published tarball (filesinpackage.json).
Public API
The package entry point exports the grouping functions, the error model, and all types:
import {
grp, // auto-selects the engine by discharge year
grp2024,
grp2025,
grp2026,
GroupingError,
BlockingErrorGroup,
RUMEnums // TYPE_ETAB, RUM_ENTRY_MODE, RUM_EXIT_MODE, ...
} from '@beparallel/grouping'The RUMObject and TYPE_ETAB types used to build input are also available from the generated types path (this is how the AlphaCode valuation service imports them):
import type { RUMObject } from '@beparallel/grouping/dist/types/utils'Year selection
grp() inspects the discharge dates on the rumObjects and routes to the matching engine:
- 2026 stays →
grp2026(serializes RUMs to the 023 RSS format) - 2025 stays →
grp2025(serializes to the 022 format) - 2024 stays →
grp2024(serializes to the 022 format) - No year detected → falls back to
grp2026(with a console warning)
Call grp2024 / grp2025 / grp2026 directly when you want to force a specific year.
Input
All grouping functions take a single object validated by Zod:
interface GroupingInput {
rumObjects: RUMObject[] // one or more Résumés d'Unité Médicale
typeEtab: TYPE_ETAB // 1 = PUBLIC_HOSPITAL (ex-DGF), 2 = PRIVATE_HOSPITAL (ex-OQN)
}A RUMObject mirrors an ATIH RSS line. Dates are DDMMYYYY strings; diagnoses are CIM-10 codes; ZAs are CCAM act rows. A real (captured) example:
const input = {
typeEtab: 1, // PUBLIC_HOSPITAL
rumObjects: [
{
FINESS: '511928447',
RSS: '22676886701954659349',
RUM: '7319798297',
birthDate: '22011959',
sex: '1',
uMed: 'RAC',
entryDate: '19122025',
entryMode: '8',
exitDate: '22122025',
exitMode: '8',
nDA: 2,
nZA: 5,
DP: 'G553', // principal diagnosis
DAs: ['M4806', 'E6694'], // associated diagnoses
ZAs: [
// CCAM acts
{ date: '19122025', code: 'LFFA001', phase: '0', act: '1', mod: 'JK', AN: '1', qty: '1' },
{ date: '19122025', code: 'LFAA002', phase: '0', act: '4', diag: '1', AN: '2', qty: '1' }
]
}
]
}
const result = grp(input) // → grp2025 (2025 discharge)The full field set lives in RUMObjectSchema (src/utils/types/rum.types.ts); the enums (entry/exit modes, sex, RAAC, etc.) are in src/utils/types/rum.enums.ts and re-exported as RUMEnums. For more complete examples, browse the captured cases under src/tests/fixtures/.
Output
Each function returns a year-specific result (Grp2024Result / Grp2025Result / Grp2026Result, see src/exports/types/) shaped like:
interface Grp2026Result {
returnCode: number // raw ATIH return code
returnError: GroupingError | null // mapped error (null when grouping succeeded)
grp_res: {
// classification result
cmd: string // Catégorie Majeure de Diagnostic
ghm: string // Groupe Homogène de Malades
version: string
cret: string
}
ghs_val: { ghs: string /* + supplement/billing counters */ }
rss_inf: { agea: number; dstot: number /* + diag/act lists, signatures */ }
}Error model
When the engine cannot group a stay it returns a non-zero returnCode, which is mapped to a GroupingError via the dictionary in src/exports/error-management/grouping-errors.dict.ts:
interface GroupingError {
code: number
errorName: string
scope: string | null
blockingErrorGroup: BlockingErrorGroup | null // 90Z00Z, 90Z01Z, 90Z02Z, 90Z03Z
description: string
}BlockingErrorGroup distinguishes mandatory-control errors from group-1/group-2 blocking errors and FG implementation errors. On success, returnError is null and returnCode is 0.
How It's Built
The build pipeline has two stages: native C compilation and TypeScript compilation. Prebuilt binaries are distributed via GitHub Releases so consumers don't need a local C toolchain.
Native C Compilation
node-gyp compiles the ATIH C source files defined in binding.gyp into .node binary addons. Four targets are built:
- grp2026 -- the 2026 grouping engine (FG distribution with 2025+2026 stays;
fgmco_2026) - grp2025 -- 2025 stays via the same FG stack as 2026 (
fgmco_2026+ SRCV2025/2026) - grp2024 -- 2024 stays (
fgmco_2025+ SRCV2024/2025) - ficum -- FICUM control module (UM authorization file generation/validation)
Build locally with:
pnpm build:c:nativeTypeScript Compilation
tsc compiles the src/ directory into CommonJS output in dist/, with type declarations emitted to dist/types/.
pnpm build:tsLocal Development
# Use the pinned Node version (.nvmrc → 24.14.0)
nvm use 24
pnpm install
# Install prebuilt native binaries. `prebuild:install` resolves them in this order:
# 1. If GITHUB_TOKEN is set -> download from the GitHub Release.
# 2. Otherwise -> extract a matching tarball committed under prebuilds/.
# 3. If neither works -> prints guidance to build from source.
pnpm prebuild:install
# Optional: only needed for the remote download path
# cp .env-example .env.dev # set GITHUB_TOKEN
# export GITHUB_TOKEN=... # token with access to the GitHub Releases
# ...or compile the native modules from source (needs python3, make, g++)
pnpm build:c:native
pnpm testRunning Tests
Tests require the native C modules to be compiled first. The pretest script handles this automatically:
pnpm testThis runs pnpm build:c:native (via the pretest hook) then jest. To skip the native rebuild when the C code hasn't changed:
npx jestTo run only the grouping fixture suite:
pnpm test:fixturesTo run a specific test file:
npx jest src/tests/grouping.test.ts
npx jest src/tests/ficum.test.tsTo update snapshots after an intentional behavior change:
npx jest --updateSnapshotTest fixtures
The grouping suite is driven by captured .case.json fixtures under src/tests/fixtures/**, auto-discovered by src/tests/fixtures/loader.ts and asserted by src/tests/grouping.test.ts.
Fixtures are generated from the AlphaCode compare-to-reference-grouping admin API response using pnpm capture-fixtures. See tools/grouping-fixtures/README.md for the full capture workflow, the Metabase baseline query, and where matches vs. mismatches land.
Prebuilt Binaries
To avoid requiring consumers to compile C code locally, the project uses prebuild and prebuild-install plus a small wrapper that adds a local-tarball fallback:
- Contents: Each tarball includes all native targets from
binding.gyp—build/Release/grp2024.node,grp2025.node,grp2026.node, andficum.node—under the same paths as a localnode-gypbuild. Thebinary.module_namefield inpackage.json(grp2026) is the asset nameprebuild-installresolves for this package; it does not mean only that single file is shipped. - Publishing: On a version tag,
.github/workflows/prebuild.ymlcreates a GitHub Release and runsprebuildon ubuntu-latest and macos-latest (Node ABI aligned with CI), uploading one tarball per OS/arch matrix row. The same workflow also supportsworkflow_dispatch(manual run), in which case it builds the same matrix but uploads to workflow artifacts instead of a release—so maintainers can pull them locally withpnpm prebuild:downloadand commit them underprebuilds/. - Installing: Consumers run
pnpm prebuild:install(seescripts/install-prebuild.cjs). The wrapper resolves the binary in this order:- If
GITHUB_TOKENis set, delegates toprebuild-install -T $GITHUB_TOKEN(download from the GitHub Release host). - Otherwise (or on failure), detects libc via
detect-libcand walks an ordered list of candidate tarballs underprebuilds/<scope>/:- glibc Linux:
linuxglibc-{arch}thenlinux-{arch} - macOS / other:
{platform}-{arch}
- glibc Linux:
- The first existing candidate is extracted into the package root, populating
build/Release/. If none exist, the wrapper exits with guidance pointing topnpm prebuild:create(build a local tarball) orpnpm build:c:native(compile from source). Usepnpm prebuild:install:remoteto bypass the wrapper and run the originalprebuild-install -T $GITHUB_TOKENdirectly.
- If
- Committed tarballs: The matrix produced by a manual
prebuild.ymlrun can be committed underprebuilds/and is shipped via thefilesarray inpackage.json. End consumers on a matchingnode_abi+platform+archtherefore do not need a token to install. The pinned Node ABI for the committed set is the value of.nvmrcat release time (currently 24.14.0). - Postinstall:
postinstallrunspnpm build, which compiles TypeScript only. Downloading prebuilds is not automatic; runprebuild:installafter install, or compile everything from source withpnpm build:c:nativein the package directory.
CI Pipeline
| Workflow | Trigger | Purpose |
| ------------------------------ | --------------------------- | ---------------------------------------------------------------------------------------- |
| prebuild.yml | Push tag v* / manual | Tag push: creates a GitHub Release and uploads prebuilts (macos x64/arm64, linux glibc x64/arm64). Manual dispatch: builds the same matrix and uploads workflow artifacts (download with pnpm prebuild:download) |
| test.yml | Pull request | Runs tests using the Node version from .nvmrc |
Package Manager
The project uses pnpm, pinned via corepack through the packageManager field in package.json.
Installation
- Install the package
pnpm add @beparallel/grouping- (Optional) Export a GitHub Token
The package ships committed prebuilt tarballs under prebuilds/ for the supported node_abi + platform + arch combinations, so a token is not required when your runtime matches that set. Only set a token if you need to download a fresh build from the GitHub Release host (e.g. on a Node version or arch that isn't covered):
export GITHUB_TOKEN=your_github_tokenor set it in the .env.dev file and run dotenv -f .env.dev run XXX.
- Install the native module
cd node_modules/@beparallel/grouping && pnpm prebuild:installprebuild:install will try GITHUB_TOKEN first if set, and otherwise extract the committed local tarball. If neither path produces binaries it prints guidance for pnpm prebuild:create or pnpm build:c:native.
In the AlphaCode monorepo this step is wired into the valuation service as the grouping:build script.
Requirements
- Node.js 24.0.0 or higher (the repo pins 24.14.0 in
.nvmrc) - Compatible with macOS and Linux (Windows is not supported)
- For native grouping: either run
prebuild:installafter adding the dependency (downloads compiled.nodefiles), or install a C toolchain (python3,make,g++) and runpnpm build:c:nativeinside the package
📚 Additional Resources
Release process
- Release process: doc/package/publish.md
- Adding a new grouping year (buy/download from ATIH + non-regression fixtures): doc/package/adding-a-new-year.md
ATIH
- Format of the RSS grouped data: doc/atih/122_format.md
- Grouping algorithm: doc/atih/grouping_method.md
- UM authorization file format (FICUM input): doc/atih/um_authorization_file_format.md
The non-grouped 022/023 RSS serialization is implemented in code under src/utils/rum/formats/ rather than a standalone doc.
