@beparallel/grouping

中文文档
1.3.1发布于 2026/10/14TypeScriptESM + CJS近 30 天下载 1746

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

原始使用文档
文档目录24 节
  1. Overview
  2. French Medical Coding Context
  3. Technical Architecture
  4. Repository layout
  5. Public API
  6. Edition selection: grpMco
  7. Input
  8. Output
  9. Error model
  10. FG SMR (soins médicaux et de réadaptation)
  11. How It's Built
  12. Native C Compilation
  13. TypeScript Compilation
  14. Local Development
  15. Running Tests
  16. Test fixtures
  17. Prebuilt Binaries
  18. CI Pipeline
  19. Package Manager
  20. Installation
  21. Requirements
  22. 📚 Additional Resources
  23. Release process
  24. ATIH

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:

  1. Native C/C++ Module: The core ATIH grouping engine in the atih/ folder (see doc/atih/grouping_method.md)
  2. JavaScript Interface: High-level Node.js API in src/ that converts JS objects to ATIH RSS strings and parses the native result
  3. Type Definitions: TypeScript definitions for development support
  4. Runtime data: ATIH reference tables shipped with the package under tables/V2024, tables/V2025, tables/V2026

Repository layout

PathPurpose
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
atih/smr/, tables/SMR/{2025,2026}/, lib/SMR/FG SMR: ATIH sources (unmodified), N-API wrapper, binary tables, label files (see FG SMR)
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. The tables/, lib/, and txt/ folders are part of the published tarball (files in package.json).

Public API

The package entry point exports the grouping functions, the error model, and all types:

import {
  grpMco, // grpMco(input) picks the engine from the discharge dates; grpMco(input, { edition }) forces one
  selectMcoEdition,
  MCO_EDITIONS,
  McoEngineUnavailableError,
  grp, // deprecated: like grpMco(input), but silently falls back to 2026
  grp2024,
  grp2025,
  grp2026,
  GroupingError,
  BlockingErrorGroup,
  RUMEnums // TYPE_ETAB, RUM_ENTRY_MODE, RUM_EXIT_MODE, ...
} from '@beparallel/grouping'

Importing the package loads no native addon: each .node file is loaded by the first call that needs it, and a missing one throws there, with its expected path under build/Release/ and how to get it (pnpm prebuild:install or pnpm build:c:native).

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'

Edition selection: grpMco

grpMco(input, options?) groups one stay with one FG MCO edition (MCO_EDITIONS = '2024', '2025', '2026') and returns that engine's result plus engineYear (2024 | 2025 | 2026, the engine that ran; type GrpMcoResult):

  • grpMco(input, { edition: '2025' }) runs exactly grp2025(input), whatever the discharge dates. An edition outside MCO_EDITIONS ('2027', a number...) throws McoEngineUnavailableError.
  • grpMco(input) runs selectMcoEdition(input.rumObjects): the first of 2026, 2025, 2024 that ends any RUM exitDate (DDMMYYYY; RUM without exitDate are ignored). A stay with discharge dates in two years goes to the newest one (31122025 + 02012026 → '2026'). When no discharge date matches a shipped edition (none at all, or 2023, 2027...), it throws McoEngineUnavailableError naming the dates seen and the shipped editions.
  • selectMcoEdition(rumObjects) is exported so a caller can know (or log) the edition before grouping, without re-implementing the rule.
  • Input is validated by GrpMcoInputSchema (zod, strict at the top level: { rumObjects, typeEtab }, 1 to 99 RUM); a ZodError is thrown otherwise. The RUM fields are checked by the engine (error codes), as before.

Each edition serializes the RUMs to its own format: 023 for 2026, 022 for 2025 and 2024. grp2024 / grp2025 / grp2026 remain available and also return engineYear.

grp(input) is deprecated: it applies the same rule as grpMco(input) but, when no discharge date matches, logs a console warning and silently runs the 2026 engine. It keeps that behaviour for existing callers; use grpMco(input) (or grpMco(input, { edition })) instead.

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 = grpMco(input) // → 2025 engine (2025 discharge), result.engineYear === 2025

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/), plus engineYear, shaped like:

interface Grp2026Result {
  engineYear: 2026 // engine that ran (2024 | 2025 | 2026)
  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.

FG SMR (soins médicaux et de réadaptation)

The package also wraps the ATIH FG SMR (distribution SourcesFGSMR2026_15, which carries the 2025 and 2026 engines). A stay is a suite of weekly RHS; the engine returns a GME (e.g. 0871UC1: GN 0871, readaptation level U, heaviness level C, severity 1) and a GMT (tariff group, e.g. 0858; 9999 = not valued).

import { grpSmr, grpSmr2025, grpSmr2026, RHSObject, TYPE_ETAB, toSmrWeek } from '@beparallel/grouping'

const result = grpSmr({ rhsObjects: weeks /* RHSObject[], one per week, sorted */, typeEtab: TYPE_ETAB.PUBLIC_HOSPITAL })
result.gme // '0871UC1' (HC stays; HTP stays: per RHS in result.rhs[i].htp)
result.gmt // '0858'
result.severity // 1
result.returnCode // 0, or the smallest blocking error code (HTP: smallest non-zero rhs[i].returnCode)
result.returnError // SmrGroupingError | null (label from LIBERR.TXT)
result.rhs // per-week rows: GME, return code, errors, 90-day threshold position, weekly GMT...
  • Input: RHSObject / RHSObjectSchema (src/utils/rhs/rhs.types.ts) is a named-field model of one RHS, serialised to the fixed-width M0D format (m0d-format.ts; each field's M0D position is in its JSDoc). Dates are DDMMYYYY; weekNumber is WWYYYY (ISO week + ISO year, see toSmrWeek()); presenceDays is 7 × 0/1, Monday to Sunday; CIM-10 codes are written without dot. Input is validated with zod (GrpSmrInputSchema, strict: unknown keys are errors; one smrStayNumber, weeks sorted, each line within the 38,210-character engine buffer; medical validity comes back as engine error codes).
  • Engine choice: grpSmr2025 / grpSmr2026 force an engine (annee_transmission = that year). grpSmr follows ATIH's dispatcher fgsmrmain.c: the last RHS decides HC vs HTP; HC goes to FG 2026 when its last week is ≥ 01/2026, else FG 2025, and is regrouped with FG 2025 when its 90th presence day (or its discharge) is before 01/01/2026; HTP RHS follow their own week (split around week 01/2026). It throws SmrEngineUnavailableError when FG 2024 (weeks before 09/2025) or FG 2027+ would be needed. result.routing says which rule applied. grpSmr(input, { edition: '2025' | '2026' }) forces an edition (same result as grpSmr2025 / grpSmr2026); an edition not in SMR_EDITIONS throws SmrEngineUnavailableError instead of falling back to the dispatcher. Edition 2025 starts at ISO week 09/2025 (Monday 24 February 2025, SMR_FIRST_WEEK_2025), one week before the 2025 campaign's official start (Monday 3 March 2025, week 10).
  • Valuation: each part of a stay in its own campaign. grpSmr reproduces fgsmrmain.c for routing, GMEs, return codes, errors and every 2026 value, but deliberately not for the 2025 parts. ATIH's executable runs FG 2025 in 2026-transmission mode (annee_transmission = 2026): for an HC stay regrouped N-1 it reports the stay GMT 9999 and, from the 90-day threshold on, the weekly GMT 9999 with 0 days for the weeks before 01/2026; HTP RHS of 2025 weeks get 0 valued days. grpSmr returns the real FG 2025 values there (FG 2025 run with annee_transmission = 2025), and in an HTP split the week-01/2026 row carries the FG 2025 valuation of its 2025 days in previousEditionValuation (add it to the row's own 2026 valuation). Price the stay-level values (gmt, hc) with the tariff of engineYear / version, each weekly GMT with the tariff of rhs[i].weeklyGmtEngineYear, each HTP row with the tariff of rhs[i].engineYear (and its previousEditionValuation with that part's engineYear), and do not bill again in 2026 what was billed in the 2025 campaign. src/tests/grouping-smr-reference.test.ts compares grpSmr with outputs of fgsmrmain.c and pins these differences.
  • Mode: mode: SmrGroupingMode.VALORISATION (default, « groupage 1 », for billing) or DESCRIPTION (« groupage 2 », GMEd).
  • Native side: one addon grpsmr.node (both engines), separate from the MCO addons; tables in tables/SMR/<year>/. The ATIH files under atih/smr/SRCV2025|SRCV2026 are byte-identical to the distribution; portability is handled in binding.gyp (see the comments on the grpsmr target) and atih/smr/compat/malloc.h, and .gitattributes keeps git from normalising their CRLF line endings. Engine state is process-global: do not call the SMR functions from several worker_threads at once.

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:

  • grpmco -- FG MCO 2025 + 2026 in one addon (FG distribution with 2025+2026 stays: fgmco_2026 + SRCV2025/2026, compiled once), exporting grp_2025_wrapper (used by grp2025) and grp_2026_wrapper (used by grp2026). The two wrappers are unmodified: grp_mco_2025_unit.c / grp_mco_2026_unit.c compile each one with its Init renamed and its NAPI_MODULE removed, and grp_mco_module.c registers the module. (Before, the same sources were linked twice, into grp2025.node and grp2026.node.)
  • grp2024 -- 2024 stays (fgmco_2025 + SRCV2024/2025)
  • grpsmr -- FG SMR 2025 + 2026 (atih/smr/, see FG SMR)
  • ficum -- FICUM control module (UM authorization file generation/validation)

Build locally with:

pnpm build:c:native

TypeScript Compilation

tsc compiles the src/ directory into CommonJS output in dist/, with type declarations emitted to dist/types/.

pnpm build:ts

Local 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 test

Running Tests

Tests require the native C modules to be compiled first. The pretest script handles this automatically:

pnpm test

This runs pnpm build:c:native (via the pretest hook) then jest. To skip the native rebuild when the C code hasn't changed:

npx jest

To run only the grouping fixture suite:

pnpm test:fixtures

To run a specific test file:

npx jest src/tests/grouping.test.ts
npx jest src/tests/ficum.test.ts

To update snapshots after an intentional behavior change:

npx jest --updateSnapshot

Test 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, grpmco.node, grpsmr.node, and ficum.node—under the same paths as a local node-gyp build (scripts/install-prebuild.cjs checks for these four). The binary.module_name field in package.json (grpmco) is the asset name prebuild-install resolves for this package; it does not mean only that single file is shipped.
  • Publishing: On a version tag, .github/workflows/prebuild.yml creates a GitHub Release and runs prebuild on ubuntu-latest and macos-latest (Node ABI aligned with CI), uploading one tarball per OS/arch matrix row. The same workflow also supports workflow_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 with pnpm prebuild:download and commit them under prebuilds/.
  • Installing: Consumers run pnpm prebuild:install (see scripts/install-prebuild.cjs). The wrapper resolves the binary in this order:
    1. If GITHUB_TOKEN is set, delegates to prebuild-install -T $GITHUB_TOKEN (download from the GitHub Release host).
    2. Otherwise (or on failure), detects libc via detect-libc and walks an ordered list of candidate tarballs under prebuilds/<scope>/:
      • glibc Linux: linuxglibc-{arch} then linux-{arch}
      • macOS / other: {platform}-{arch}
    3. The first existing candidate is extracted into the package root, populating build/Release/. If none exist, the wrapper exits with guidance pointing to pnpm prebuild:create (build a local tarball) or pnpm build:c:native (compile from source). Use pnpm prebuild:install:remote to bypass the wrapper and run the original prebuild-install -T $GITHUB_TOKEN directly.
  • Committed tarballs: The matrix produced by a manual prebuild.yml run can be committed under prebuilds/ and is shipped via the files array in package.json. End consumers on a matching node_abi + platform + arch therefore do not need a token to install. The pinned Node ABI for the committed set is the value of .nvmrc at release time (currently 24.14.0).
  • Postinstall: postinstall runs pnpm build, which compiles TypeScript only. Downloading prebuilds is not automatic; run prebuild:install after install, or compile everything from source with pnpm build:c:native in the package directory.

CI Pipeline

WorkflowTriggerPurpose
prebuild.ymlPush tag v* / manualTag 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.ymlPull requestRuns tests using the Node version from .nvmrc

Package Manager

The project uses pnpm, pinned via corepack through the packageManager field in package.json.

Installation

  1. Install the package
pnpm add @beparallel/grouping
  1. (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_token

or set it in the .env.dev file and run dotenv -f .env.dev run XXX.

  1. Install the native module
cd node_modules/@beparallel/grouping && pnpm prebuild:install

prebuild: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:install after adding the dependency (downloads compiled .node files), or install a C toolchain (python3, make, g++) and run pnpm build:c:native inside the package

📚 Additional Resources

Release process

ATIH

The non-grouped 022/023 RSS serialization is implemented in code under src/utils/rum/formats/ rather than a standalone doc.

版本与发布记录

@beparallel/grouping 共发布 103 个版本,首次发布于 2025/06/07;过去 12 个月发布了 28 个版本。

版本发布日期距今
1.3.1latest2026/10/1427 天前
1.3.02026/10/1228 天前
1.2.12026/09/221 个月前
1.2.02026/09/072 个月前
1.1.12026/08/262 个月前
1.1.02026/08/252 个月前

关于 @beparallel/grouping 的常见问题

@beparallel/grouping 是什么?

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

如何安装 @beparallel/grouping?

在项目目录运行 npm install @beparallel/grouping,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 grouping-build,可以用 npx grouping-build 直接运行。

@beparallel/grouping 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@beparallel/grouping 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@beparallel/grouping 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@beparallel/grouping 可以商用吗?

@beparallel/grouping 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@beparallel/grouping 的下载量有多少?

根据 npm 官方下载统计,@beparallel/grouping 在 2026/10/08 – 2026/11/06 这 30 天内被下载 1,746 次,日均约 58 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@beparallel/grouping 还在维护吗?

最新版本 1.3.1 发布于 2026/10/14(27 天前);至今共发布 103 个版本,其中过去 12 个月发布了 28 个;首次发布于 2025/06/07。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。