Repository navigation
feat: add hasICD10Code and harden lookup input handling - #291
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| node-version: [22, 24] |
There was a problem hiding this comment.
note: engines allows Node 22, but CI only ran 24. npm run test:coverage also passes locally on 22.23.4.
| - v3.1.0 - `ICD10_CM_RELEASE` export with the bundled fiscal year, effective date, and code count. Node.js 22+ declared in `engines`, `package.json` exported, expanded npm keywords. API, CDN, and versioning docs, plus contributing and security guides | ||
| - v3.0.0 - Data updated to the October 1, 2026 ICD-10-CM release (74,879 codes, from the FY2027 set) | ||
| - v2.0.0 - Data updated to the April 1, 2026 ICD-10-CM release (74,719 codes, from the FY2024 set). TypeScript 7 (native compiler) toolchain, ESLint replaced with oxlint + Prettier, dropped tsup for plain tsc (zero-bundler dual ESM/CJS), dataset shipped once and lazy-loaded (~50% smaller install, faster imports), fixed broken `require()` entry point | ||
| - v2.0.0 - Data updated to the April 1, 2026 ICD-10-CM release (74,719 codes, from the FY2026 set). TypeScript 7 (native compiler) toolchain, ESLint replaced with oxlint + Prettier, dropped tsup for plain tsc (zero-bundler dual ESM/CJS), dataset shipped once and lazy-loaded (~50% smaller install, faster imports), fixed broken `require()` entry point |
There was a problem hiding this comment.
note: the April 2026 release is the FY2026 set, not FY2024. Inferred from the 74,719 code count, so worth checking against the CDC files.
| const codePattern = /^[A-Z][0-9A-Z]{2,6}$/; | ||
| const linePattern = /^(\S+)\s+(.+?)\s*$/; | ||
|
|
||
| // Function to parse the ICD file content | ||
| const parseICDFile = (content: string): ICD10Dictionary => { | ||
| const lines = content.split("\n"); // Split content into lines | ||
| const dictionary: ICD10Dictionary = {}; | ||
|
|
||
| for (const line of lines) { | ||
| const [code, ...descriptionParts] = line.split(/\s+/); // Split by whitespace (tab or spaces) | ||
| const description = descriptionParts.join(" "); // Rejoin the description parts | ||
| for (const [index, line] of content.split(/\r?\n/).entries()) { | ||
| if (line.trim() === "") { | ||
| continue; | ||
| } | ||
|
|
||
| const match = linePattern.exec(line); | ||
| const [, code, description] = match ?? []; | ||
|
|
||
| if (code && description) { | ||
| dictionary[code] = description.trim(); // Add to dictionary | ||
| if (!code || !description || !codePattern.test(code)) { | ||
| throw new Error( | ||
| `Line ${index + 1} is not "<code> <description>": ${line}`, | ||
| ); | ||
| } | ||
|
|
||
| if (Object.hasOwn(dictionary, code)) { | ||
| throw new Error(`Line ${index + 1} repeats code ${code}`); | ||
| } | ||
|
|
||
| dictionary[code] = description; | ||
| } | ||
|
|
||
| return dictionary; |
There was a problem hiding this comment.
note: the parser now throws on a malformed line or a repeated code instead of skipping or overwriting it. The pattern allows letters in the second character because FY2027 added QA-prefixed codes. Output for the current source is byte-identical.
| "esModuleInterop": true, | ||
| "forceConsistentCasingInFileNames": true, | ||
| "strict": true, | ||
| "noUncheckedIndexedAccess": true, |
There was a problem hiding this comment.
note: makes the dataset lookup in src/index.cts type as string | undefined instead of relying on the declared return type.
There was a problem hiding this comment.
🟡 Changes recommended
The parser accepts whitespace-only descriptions and writes blank values into the generated dataset, violating the trimmed, non-empty description invariant.
1 open issue
What changed in this PR
This PR adds hasICD10Code and hardens input handling in the ICD-10-CM lookup package without changing the bundled dataset.
Changes:
- Adds the boolean lookup helper, non-string input handling, and internal whitespace normalization.
- Exports the dataset JSON and documents category-header behavior.
- Strengthens parser validation, expands tests, and adds Node.js 22/24 CI coverage.
| File | Description |
|---|---|
tsconfig.json |
Enables unchecked indexed-access checking. |
tests/package.test.cjs |
Tests packaged helper, JSON export, and types. |
tests/index.test.cjs |
Covers input handling, helper behavior, and data integrity. |
tests/esm.test.mjs |
Checks the helper’s ESM export. |
src/index.mts |
Re-exports hasICD10Code for ESM consumers. |
src/index.cts |
Adds the helper and hardens normalization. |
scripts/parseICD.ts |
Validates records and anchors the output path to the script. |
README.md |
Documents new behavior and corrects release-history text. |
package.json |
Exports dataset JSON and bumps version to 3.2.0. |
package-lock.json |
Synchronizes the package version. |
.github/workflows/test.yml |
Runs CI on Node.js 22 and 24. |
Review effort: Balanced
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

What and why
A review of the package turned up input-handling gaps and missing docs. The agent-tool use case in the README makes the first one realistic: a model can call
getICD10Descriptionwith a missing or non-string argument, and it threw aTypeError.undefinedfrom lookups and""fromnormalizeICD10CodehasICD10Code(code)returns a boolean, so callers don't need!== undefinedA00. 1resolvesdata/icd10.min.jsonis exported from the package. Importing it by path used to fail withERR_PACKAGE_PATH_NOT_EXPORTED, which left bundlers and edge runtimes with only the CDNE11aren't in the dataset, since the CMS file lists complete codes only. A grounding check would otherwise read a real category as a hallucinated codeVersion is 3.2.0. The data is unchanged, and regenerating it with
npm run parse-icdproduces an identical file. The one behavior change consumers can see is the whitespace stripping: inputs likeA 00.0that returnedundefinednow resolve.Testing
npm testnpm run lintAdded tests for non-string input, category headers, prototype property names, whitespace normalization,
hasICD10Code, the data export through the packed tarball, and a format check on every dataset key and description.