@cosyte/ncpdp

中文文档
0.1.6发布于 2026/11/10MITTypeScriptESM + CJS构建来源已验证近 30 天下载 3523

NCPDP parser, serializer, and builder for Node.js and TypeScript: SCRIPT ePrescribing and Telecom pharmacy claims, lenient on parse, spec-clean on emit.

原始使用文档
文档目录17 节
  1. Why this exists
  2. Status
  3. Install
  4. Dependencies
  5. Usage
  6. Emit and read a Telecom billing claim
  7. Read a SCRIPT NewRx
  8. API
  9. Dates and times
  10. Compatibility
  11. Documentation
  12. Support
  13. Security
  14. PHI and safety
  15. Contributing
  16. License
  17. Trademarks
The Cosyte logo on its own white ground: the icon beside the word Cosyte.

@cosyte/ncpdp

Read a vendor-quirky Telecom pharmacy claim or SCRIPT ePrescription in one line, without buying the standard.

npm version CI License: MIT Node

NCPDP parser, serializer, and builder for Node.js and TypeScript: SCRIPT ePrescribing and Telecom pharmacy claims, lenient on parse, spec-clean on emit.

Why this exists

NCPDP is two structurally unrelated standards under one brand: SCRIPT, an XML ePrescribing format, and the Telecommunication Standard, a control-character-framed pharmacy claim format. The Implementation Guides for both are purchased products, so the usual route for a Node team is to hand-roll a reader against a guide somebody had to buy: an element walk over fast-xml-parser for SCRIPT, a String.split on the FS/GS/RS separators plus hard-cut byte offsets for Telecom. That works until real input arrives, and then a vendor quirk is an exception thrown in the middle of a dispense, an off-by-one offset is a wrong field, and a currency amount parsed with parseFloat is a wrong paid amount. This package is the other choice: a lenient reader that turns quirks into positioned warnings instead of failures, a conservative emitter that only ever writes spec-clean output, and quantities and money handled string-wise so binary floating point can never corrupt a value.

Status

The 0.1 line. The public API is settled and stable enough to depend on. The exported functions, the shape of their arguments, the model fields they return, and the stable warning codes consumers branch on are all covered by semantic versioning from this release onward; renaming a warning code is a breaking change and is treated as one.

Two public surfaces are still moving, and both are additive views rather than the parse itself:

  • The structured SIG view (medication.sig). Explicitly lossy and explicitly labeled: the free-text sigText is always authoritative and is never overwritten, and a structured component decodes only where a published field label grounds its element name. That grounded-name list can lose a name when its grounding is re-examined, which moves a component from coded to absent.
  • The wire-code label tables (known on a reject, DUR, or reason code). A label ships only where a public artifact establishes it, so most codes come back as the bare wire code with known: false. Labels are added when an artifact is found, and never guessed.

Everything else on both wire formats is settled. What the package does and does not decode is stated once, in the conformance statement.

Install

pnpm add @cosyte/ncpdp

Node.js 22 or later, tested on 22, 24, and 26 (engines.node is >=22.0.0). The package is ESM ("type": "module") and ships a CommonJS build beside it, so import and require both resolve through the same exports map. TypeScript types resolve under node16 and under legacy node10.

Dependencies

The Telecom side is zero-dependency, Node standard library only. The SCRIPT side takes a single vetted runtime dependency, fast-xml-parser, for namespace-aware XML parsing, configured XXE-safe with entity resolution disabled. No further runtime dependency is added without the same review.

Usage

Both examples below are complete programs. Each was run against this package, and the block beneath it is the output it printed, verbatim. Every patient, prescriber, member, and product value in them is synthetic.

Emit and read a Telecom billing claim

import { buildTelecomRequest, serializeTelecom, parseTelecom, claim } from "@cosyte/ncpdp/telecom";

// Emit a spec-clean Telecom B1 billing claim. Every value below is synthetic.
const wire = serializeTelecom(
  buildTelecomRequest({
    header: { transactionCode: "B1", binNumber: "999999", dateOfService: "20260115" },
    segments: [
      { segmentId: "04", fields: [{ id: "C2", value: "SYNTHCARD09" }] },
      {
        segmentId: "07",
        fields: [
          { id: "D2", value: "RX0000001" },
          { id: "E1", value: "03" },
          { id: "D7", value: "99999999999" },
          { id: "E7", value: "30000" },
          { id: "D5", value: "30" },
        ],
      },
    ],
  }),
);

// Read it back. A vendor quirk becomes a positioned warning here, never a throw.
const t = parseTelecom(wire);
const c = claim(t);

console.log(t.kind, t.header.transactionCode, t.warnings.length);
console.log(c?.cardholderId, c?.prescriptionReferenceNumber);
console.log(c?.product?.id, c?.product?.qualifier);
console.log(c?.quantityDispensed?.source, c?.quantityDispensed?.impliedDecimal);
request B1 0
SYNTHCARD09 RX0000001
99999999999 03
30000 30.000

Note the last line. Quantity Dispensed carries an implied three-place decimal, and the verbatim source is kept beside the scaled value, which is produced string-wise rather than through a float.

Read a SCRIPT NewRx

import { parseScript, newRx } from "@cosyte/ncpdp/script";

// A minimal SCRIPT NewRx. Every value below is synthetic.
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Message xmlns="http://www.ncpdp.org/schema/SCRIPT" version="2017071">
  <Header><MessageID>SYNTH-MSG-0001</MessageID></Header>
  <Body><NewRx>
    <Patient><HumanPatient>
      <Name><LastName>Testpatient</LastName><FirstName>Avery</FirstName></Name>
      <DateOfBirth><Date>1990-04-12</Date></DateOfBirth>
    </HumanPatient></Patient>
    <MedicationPrescribed>
      <DrugDescription>Amoxicillin 500 MG Oral Capsule</DrugDescription>
      <DrugCoded><ProductCode Qualifier="ND">99999999999</ProductCode></DrugCoded>
      <Sig><SigText>Take 1 capsule by mouth three times daily for 10 days.</SigText></Sig>
    </MedicationPrescribed>
  </NewRx></Body>
</Message>`;

const msg = parseScript(xml);
const rx = newRx(msg); // the NewRx body, or undefined for another transaction

console.log(msg.header.messageId, msg.warnings.length);
console.log(rx?.patient?.name?.lastName, rx?.patient?.dateOfBirth);
console.log(rx?.medication?.description, rx?.medication?.coded?.productCode?.system);
console.log(rx?.medication?.sig?.sigText);
SYNTH-MSG-0001 0
Testpatient 1990-04-12
Amoxicillin 500 MG Oral Capsule NDC
Take 1 capsule by mouth three times daily for 10 days.

More tasks, each with its own worked example, are in the how-to guides, starting with read a PBM response and build and serialize messages.

API

Five code entry points, plus @cosyte/ncpdp/package.json for tooling that reads the manifest:

importwhat it carries
@cosyte/ncpdpVERSION and every export of script, telecom, common, and profiles
@cosyte/ncpdp/scriptSCRIPT: parseScript, newRx, the response spine (status / error / verify), the lifecycle readers (rxRenewalResponse / rxChangeResponse / cancelRxResponse, approvalOf), serializeScript, buildNewRx, buildScriptResponse
@cosyte/ncpdp/telecomTelecom: parseTelecom, claim, adjudication, compound, cobOtherPayments, responseCob, requestDur, priorAuthorization, serializeTelecom, buildTelecomRequest
@cosyte/ncpdp/commonshared vocabulary: ndcValue, decimalValue, position helpers, and the warning and fatal code constants both formats share
@cosyte/ncpdp/profilesdefineProfile, partitionWarnings, and the built-in profiles.surescripts and profiles.pbm

Signatures, return types, and per-field notes are on every public export as JSDoc, so an editor shows them inline. The caveats that bite, and that no signature can show you, are these:

  • A reject always wins. A response disposition is a total function over the Transaction Response Status and the reject codes together. If any reject is present the disposition is "rejected" even when the status field claims paid, and the contradiction is surfaced rather than resolved. A consumer is never told a rejected claim was paid. An unrecognized status reads "unknown", never paid.
  • A denial is never an approval. A lifecycle outcome is read only from the response's own choice element. An unrecognized or absent outcome reads "unknown", and a malformed response carrying more than one outcome resolves denial-first. On an approvedWithChanges, read medicationPrescribed: that is the changed medication, and it is the one to dispense.
  • An Error is never a success. A SCRIPT disposition is derived only from the response body kind, and where a malformed message carries more than one response body the most conservative one wins. Codes and descriptions are surfaced verbatim; no code-to-meaning table ships.
  • Money and quantity are never floats. Every amount carries an implied two-place decimal and an optional zoned-decimal overpunch sign, and quantity an implied three-place decimal. Both are applied string-wise with the verbatim source kept beside the result. Anything unexpected is preserved with isValid: false and no interpreted value rather than guessed at.
  • Nothing is silently dropped. Unknown segments, unknown fields, and malformed tokens are preserved verbatim and warned. Each decoded transaction sits at its own index with its own segments, byte offset, and warnings, and a malformed later transaction costs only itself. Every compound ingredient, every coordination-of-benefits money row, and every returned DUR alert is surfaced, never merged or truncated; a declared count that disagrees with the decoded count raises a warning and drops nothing. A declared transaction count is reported, never enforced.
  • Prior authorization is presence, not adjudication. It reports that the segment was submitted and echoes the type and number. It never decides whether an authorization is valid or honored.
  • The structured SIG is additive and lossy. sigText stays the source of truth and is never reconciled against the structured view; where the two disagree, both are surfaced as-is. Every component is tagged coded, derived, or absent, an absent one is never inferred from the free text, and an ambiguous dose reads absent rather than being guessed.
  • Emit is canonical form, not byte identity. The read is lossy, so emit reproduces the modeled content: serialize(parse(serialize(x))) is byte-identical to serialize(x) and parse(serialize(x)) is structurally equal to x. The builders refuse a message that is invalid by construction with a typed error rather than emitting something malformed, and the offending value is never echoed in the error. Serializing a SCRIPT transaction the package does not model throws rather than writing a document with the body deleted: test body.kind first and relay the original bytes.
  • Only structural corruption throws. Empty input, non-XML, a missing message root, a pre-XML legacy version, absent framing bytes, and an unsupported version stamp are typed fatal errors. Everything else is a warning with a stable code and a position.

Dates and times

Every date-bearing field on a parsed model is, and stays, the verbatim string that arrived on the wire. Nothing below rewrites one. The conversions are opt-in: you reach for them when you want a date as parts, as ISO-8601, or as an instant, and until you do, header.dateOfService and claim.dateOfBirth are exactly the characters the sender sent.

dateValue decodes a wire string into a value; toObject, toISO, and toDate read that value. The three names are the same in every @cosyte/* parser.

import { dateValue, toObject, toISO, toDate } from "@cosyte/ncpdp";

const dos = dateValue(t.header.dateOfService); // "20260629"

toObject(dos); // { year: 2026, month: 6, day: 29 }: month is 1 to 12, never 0 to 11
toISO(dos); // "2026-06-29": truncated to the precision stated, nothing appended
toDate(dos); // undefined: see the timezone rule below
toDate(dos, { assumeOffsetMinutes: 0 }); // 2026-06-29T00:00:00.000Z
toDate(dos, { assumeOffsetMinutes: -300 }); // 2026-06-29T05:00:00.000Z

One wire form is decoded: CCYYMMDD. It is the only date form this package declares, on Date of Service (401-D1) in the Transaction Header and on Date of Birth (304-C4) in the Patient segment. Every other date-bearing or time-bearing field is carried verbatim with no form stated for it: the SCRIPT <SentTime>, <DateOfBirth>, and <WrittenDate> values, and the Telecom Other Payer Date (443-E8) and Previous Date Of Fill (530-FU) values. dateValue answers undefined for those rather than guessing a form, because the documents that would settle them are purchased products this package does not redistribute or cite, and a date decoded from a guess is a wrong answer that nothing throws to announce. The fields themselves are untouched and still readable.

A day the calendar does not have is refused, never rolled over. dateValue("20240230") is undefined, and so is a DateValue you build or spread yourself carrying those same components: the conversions apply the same 4/100/400 leap rule the decoder applies, so both routes answer alike for the same eight digits. Nothing here renders "2024-02-30", which every ISO-8601 reader reads back as 1 March, and toDate never returns a day in the following month.

toDate never guesses a zone. No form decoded here carries a UTC offset, so the zone is never determinate on its own: with no assumeOffsetMinutes you get undefined, and the host machine's timezone is never read and UTC is never assumed. Supply the offset (an explicit 0 means "treat this naive value as UTC") or get nothing back. That refusal is the point: a date of birth resolved to a guessed zone lands on the previous day in every negative-offset zone, and it does it silently.

toObject returns only the components the value stated, so Object.keys() recovers its precision. Deleting offsetMinutes leaves an object Temporal.PlainDate.from and luxon's DateTime.fromObject accept as-is. Neither library is a dependency here; this package stays zero-runtime-dependency apart from its XML parser.

Using two @cosyte parsers in one file

The three names are deliberately identical across the suite, so importing two parsers into one module means aliasing or namespacing them:

import { toISO as ncpdpToISO, dateValue } from "@cosyte/ncpdp";
import { toISO as hl7ToISO, parseDtm } from "@cosyte/hl7";

ncpdpToISO(dateValue(c.dateOfBirth)); // c: the Telecom claim view
hl7ToISO(parseDtm(dtm)); // dtm: an HL7 v2 timestamp string

// or namespace them instead
import * as ncpdp from "@cosyte/ncpdp";

Compatibility

Standard versions. Which version of each standard is decoded, which stamp is recognized without being decoded, the public section that adopts each one, and the date that adoption ends, are stated once and only once, in the conformance statement. They are deliberately not restated here, because a second copy is a copy that goes stale.

Vendor quirks. Trading-partner and companion-guide differences are handled through the profile system: defineProfile() plus the built-in profiles.surescripts for SCRIPT and profiles.pbm for Telecom, both authored through that same public API and grounded in real tolerance fixtures rather than in a guess. Attach one with parseScript(xml, { profile }) or parseTelecom(raw, { profile }). Profiles are descriptive: they attach attribution and power warning partitioning, and output with a profile attached is byte-identical to output without one. A profile never alters the parse.

Named gaps, rather than silence. Electronic Prescribing of Controlled Substances is out of scope and belongs in a separate package: it needs DEA-regulated signature verification and a different certification posture. Parsing and emitting are whole-message only, with no streaming mode. The SCRIPT builder emits the SIG it is given and generates none from structure, and the structured SIG decode does not read arbitrary natural-language directions. Most wire-code labels do not ship, because no public artifact establishes them. No third party has tested this package, and there is no differential corpus against a reference implementation; the conformance statement says what stands in for both, and KNOWN-LIMITATIONS.md is the full do-not-over-trust list.

Documentation

The full reference lives at docs.cosyte.com/ncpdp, built from this repository's docs-content/. Start with these pages:

  • Quickstart: read a SCRIPT NewRx and a Telecom B1 claim end to end, and see what throws and what is only a warning.
  • Bill a claim and read the answer: a tutorial that builds a Telecom claim and reads a paid and a rejected answer.
  • Diagnostic codes: every fatal, warning, and build code, with what the reader or builder did.
  • Conformance statement: what each wire format decodes, the public section that adopts it, when that adoption ends, and who has tested it.
  • Troubleshooting: what to do when a parse throws, a field reads undefined, or an emit is refused, and the known limitations.
  • Known limitations: the full do-not-over-trust list.

Support

Bug reports go on the issue tracker, with a minimal reproduction built from synthetic data only. Questions and ideas go to Discussions, which covers every Cosyte repository. The organization's support page lists both.

Need an NCPDP integration built, or hosted and run for you? See our integration services.

Security

Report a vulnerability privately, through this repository's private vulnerability reporting or by email to [email protected], as the security policy describes. Never report one in a public issue, and never include real patient data.

PHI and safety

A pharmacy transaction carries PHI, and this library is built on the assumption that yours does.

Logging. The library logs nothing. There is no console call in library code, and its own diagnostics are safe to log whole: a warning or error message comes from a frozen registry keyed by code, and the factories that build one take a position and nothing else, so there is no interpolation site a document can reach. A warning's message is byte-identical to the registry entry for its code, and a test asserts exactly that. Position is an XPath for SCRIPT and a byte offset plus a two-character field id for Telecom, and its parts come only from names this library recognizes, never from a name a sender chose.

Retention. Nothing is retained. A parse is a pure function of the bytes handed to it. There is no cache, no module-level state, and no history; the frozen model returned is the only thing that outlives the call.

Writing to disk. Nothing is written, and nothing is fetched. The library opens no file and no socket at any point, and the code lists it ships are bundled snapshots compiled into the package rather than a runtime download.

The parsed model is not safe to log. Field values, drug codes, descriptions, and identifiers are exactly as sensitive as the claim or prescription they came from: that is what you asked the parser for. What is guaranteed is that the library's own structural identifiers stay bounded, so a downstream package building diagnostics out of them cannot be handed unbounded wire bytes: segment.segmentId is always two characters or empty however the transaction was made, field.id is at most two, and an unmodeled SCRIPT transaction is named only from a closed vocabulary.

What the consuming application still owns. Everything outside the call: transport and encryption, authentication, access control and audit, retention and disposal, de-identification before analytics, and its own logging. If you log the parsed model, you have logged PHI. The SCRIPT loader refuses any input carrying a <!DOCTYPE> or <!ENTITY> declaration and resolves no entities, so there is no external-entity or billion-laughs vector, but no parser can make an untrusted document safe to store.

Contributing

External pull requests. The repository is public and MIT-licensed, and it takes pull requests on the same terms as any other change. The organization's contributing guide and code of conduct apply here, and this section adds what is specific to this repository.

What a contribution must clear before merge. A pull request has to go green on the required status checks on main. Locally that is:

pnpm lint && pnpm typecheck && pnpm test && pnpm run check && pnpm run build

Lint runs at --max-warnings=0 and coverage is gated per directory at 90 percent. Two gates check the text as well as the code: no em or en dash in any tracked file or in the pull request title, body or commit messages, and no internal project identifier on a published surface. A change that alters behavior also needs a changeset, and a changeset that renames a stable warning code is describing a breaking change.

License

MIT. Copyright (c) 2026 Cosyte LLC. Full text: LICENSE.

Trademarks

@cosyte/ncpdp is an independent open-source project. Cosyte is not affiliated with, endorsed by, or sponsored by any company named in this repository or its documentation. Surescripts appears as the name of a built-in profile, because a profile cannot record whose trading-partner conventions it accommodates without naming them. See TRADEMARKS.md.

版本与发布记录

@cosyte/ncpdp 共发布 20 个版本,首次发布于 2026/07/17;过去 12 个月发布了 20 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.1.6latest2026/11/10今天
0.1.52026/11/092 天前
0.1.42026/11/082 天前
0.1.32026/11/082 天前
0.1.22026/11/082 天前
0.1.12026/11/083 天前

关于 @cosyte/ncpdp 的常见问题

@cosyte/ncpdp 是什么?

NCPDP parser, serializer, and builder for Node.js and TypeScript: SCRIPT ePrescribing and Telecom pharmacy claims, lenient on parse, spec-clean on emit.

如何安装 @cosyte/ncpdp?

在项目目录运行 npm install @cosyte/ncpdp,或使用 pnpm add / yarn add / bun add。

@cosyte/ncpdp 支持 TypeScript 吗?

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

@cosyte/ncpdp 支持 ESM 还是 CommonJS?

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

@cosyte/ncpdp 需要什么版本的 Node.js?

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

@cosyte/ncpdp 可以商用吗?

@cosyte/ncpdp 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cosyte/ncpdp 的下载量有多少?

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

@cosyte/ncpdp 还在维护吗?

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