fhir-data-utils-ts 0.2.4

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.
@@ -0,0 +1,284 @@
1
+ import { buildFlatClaimResourceEntry } from './flat-claim-resource-graph.js';
2
+ import { FhirCodeSystem, ObservationCategoryCodes, ObservationClaim, decodeObservationClaimList, } from './observation-claims.js';
3
+ export { ObservationCategoryCodes, ObservationClaim } from './observation-claims.js';
4
+ function defineCoding(system, code, display) {
5
+ return Object.freeze({ system, code, display, claim: `${system}|${code}` });
6
+ }
7
+ /** Canonical LOINC descriptors for supported vital signs. */
8
+ export const VitalSignsCodes = Object.freeze({
9
+ BodyWeight: defineCoding(FhirCodeSystem.Loinc, '29463-7', 'Body weight'),
10
+ HeartRate: defineCoding(FhirCodeSystem.Loinc, '8867-4', 'Heart rate'),
11
+ BloodPressure: defineCoding(FhirCodeSystem.Loinc, '85354-9', 'Blood pressure panel'),
12
+ SystolicBloodPressure: defineCoding(FhirCodeSystem.Loinc, '8480-6', 'Systolic blood pressure'),
13
+ DiastolicBloodPressure: defineCoding(FhirCodeSystem.Loinc, '8462-4', 'Diastolic blood pressure'),
14
+ BodyTemperature: defineCoding(FhirCodeSystem.Loinc, '8310-5', 'Body temperature'),
15
+ OxygenSaturation: defineCoding(FhirCodeSystem.Loinc, '2708-6', 'Oxygen saturation'),
16
+ RespiratoryRate: defineCoding(FhirCodeSystem.Loinc, '9279-1', 'Respiratory rate'),
17
+ });
18
+ /** Canonical UCUM descriptors for supported vital signs. */
19
+ export const VitalSignsUnits = Object.freeze({
20
+ BeatsPerMinute: defineCoding(FhirCodeSystem.Ucum, '/min', 'beats/minute'),
21
+ MillimeterOfMercury: defineCoding(FhirCodeSystem.Ucum, 'mm[Hg]', 'mmHg'),
22
+ Celsius: defineCoding(FhirCodeSystem.Ucum, 'Cel', 'degrees Celsius'),
23
+ Percent: defineCoding(FhirCodeSystem.Ucum, '%', 'percent'),
24
+ Kilogram: defineCoding(FhirCodeSystem.Ucum, 'kg', 'kilogram'),
25
+ });
26
+ /** Governed FHIR/LOINC tokens used by every channel that captures vital signs. */
27
+ export const VitalSignCode = Object.freeze({
28
+ bloodPressure: VitalSignsCodes.BloodPressure.claim,
29
+ systolic: VitalSignsCodes.SystolicBloodPressure.claim,
30
+ diastolic: VitalSignsCodes.DiastolicBloodPressure.claim,
31
+ temperature: VitalSignsCodes.BodyTemperature.claim,
32
+ heartRate: VitalSignsCodes.HeartRate.claim,
33
+ oxygenSaturation: VitalSignsCodes.OxygenSaturation.claim,
34
+ respiratoryRate: VitalSignsCodes.RespiratoryRate.claim,
35
+ bodyWeight: VitalSignsCodes.BodyWeight.claim,
36
+ });
37
+ /** UCUM-compatible unit spellings persisted in flat claims. */
38
+ export const VitalSignUnit = Object.freeze({
39
+ bloodPressure: VitalSignsUnits.MillimeterOfMercury.claim,
40
+ temperature: VitalSignsUnits.Celsius.claim,
41
+ heartRate: VitalSignsUnits.BeatsPerMinute.claim,
42
+ oxygenSaturation: VitalSignsUnits.Percent.claim,
43
+ respiratoryRate: VitalSignsUnits.BeatsPerMinute.claim,
44
+ bodyWeight: VitalSignsUnits.Kilogram.claim,
45
+ });
46
+ /**
47
+ * Builds one timestamped Observation. The device is retained as source; this
48
+ * helper deliberately never creates a professional performer or attester.
49
+ */
50
+ export function buildVitalSignObservation(input) {
51
+ const measuredAt = normalizeDateTime(input.measuredAt);
52
+ const claims = {
53
+ [ObservationClaim.Identifier]: required(input.entryId, 'vital_sign_entry_id_required'),
54
+ [ObservationClaim.Subject]: required(input.subjectReference, 'vital_sign_subject_required'),
55
+ [ObservationClaim.Status]: input.status,
56
+ [ObservationClaim.Category]: ObservationCategoryCodes.VitalSigns.claim,
57
+ [ObservationClaim.EffectiveDateTime]: measuredAt,
58
+ };
59
+ if (input.deviceReference)
60
+ claims[ObservationClaim.Device] = required(input.deviceReference, 'vital_sign_device_required');
61
+ if (input.note?.trim())
62
+ claims[ObservationClaim.Note] = input.note.trim();
63
+ if (input.userSelected !== undefined)
64
+ claims[ObservationClaim.UserSelected] = String(input.userSelected);
65
+ if (input.kind === 'blood-pressure') {
66
+ if (!validPositive(input.systolic) || !validPositive(input.diastolic) || input.systolic <= input.diastolic) {
67
+ throw new TypeError('blood_pressure_values_invalid');
68
+ }
69
+ claims[ObservationClaim.Code] = VitalSignCode.bloodPressure;
70
+ claims[ObservationClaim.CodeDisplay] = VitalSignsCodes.BloodPressure.display ?? '';
71
+ const components = Object.freeze([
72
+ buildBloodPressureComponent(input, 'systolic', input.systolic, VitalSignsCodes.SystolicBloodPressure),
73
+ buildBloodPressureComponent(input, 'diastolic', input.diastolic, VitalSignsCodes.DiastolicBloodPressure),
74
+ ]);
75
+ claims[ObservationClaim.HasMember] = components.map(component => component.reference).join(',');
76
+ const primary = buildFlatClaimResourceEntry({ entryId: input.entryId, resourceType: 'Observation', claims });
77
+ return freezeObservationGraph(primary, components);
78
+ }
79
+ else {
80
+ if (!validPositive(input.value))
81
+ throw new TypeError('vital_sign_value_invalid');
82
+ const scalar = scalarDefinition(input.kind);
83
+ claims[ObservationClaim.Code] = scalar.coding.claim;
84
+ if (scalar.coding.display)
85
+ claims[ObservationClaim.CodeDisplay] = scalar.coding.display;
86
+ claims[ObservationClaim.ValueQuantityNumber] = String(input.value);
87
+ claims[ObservationClaim.ValueQuantityUnit] = scalar.unit;
88
+ }
89
+ return freezeObservationGraph(buildFlatClaimResourceEntry({ entryId: input.entryId, resourceType: 'Observation', claims }), Object.freeze([]));
90
+ }
91
+ /** Restores component values from the canonical graph or legacy scalar/array claims. */
92
+ export function parseObservationComponents(source) {
93
+ if (isVitalSignObservationGraph(source)) {
94
+ return Object.freeze(source.components.map(component => componentQuantityFromEntry(component)));
95
+ }
96
+ const claims = source;
97
+ const codes = decodeObservationClaimList(claims[ObservationClaim.ComponentCode]);
98
+ const displays = decodeObservationClaimList(claims[ObservationClaim.ComponentCodeDisplay]);
99
+ const values = decodeObservationClaimList(claims[ObservationClaim.ComponentValueQuantityNumber]);
100
+ const units = decodeObservationClaimList(claims[ObservationClaim.ComponentValueQuantityUnit]);
101
+ if (!codes.length && claims[ObservationClaim.Code] === VitalSignCode.bloodPressure) {
102
+ const systolic = Number(claims[ObservationClaim.BloodPressureSystolicNumber]);
103
+ const diastolic = Number(claims[ObservationClaim.BloodPressureDiastolicNumber]);
104
+ if (Number.isFinite(systolic) && Number.isFinite(diastolic)) {
105
+ return Object.freeze([
106
+ Object.freeze({ code: VitalSignCode.systolic, value: systolic, unit: VitalSignUnit.bloodPressure }),
107
+ Object.freeze({ code: VitalSignCode.diastolic, value: diastolic, unit: VitalSignUnit.bloodPressure }),
108
+ ]);
109
+ }
110
+ }
111
+ if (codes.length !== values.length || codes.length !== units.length)
112
+ throw new TypeError('observation_components_not_aligned');
113
+ if (displays.length && displays.length !== codes.length)
114
+ throw new TypeError('observation_components_not_aligned');
115
+ return Object.freeze(codes.map((code, index) => {
116
+ const value = Number(values[index]);
117
+ if (typeof code !== 'string' || typeof units[index] !== 'string' || !Number.isFinite(value)) {
118
+ throw new TypeError('observation_component_invalid');
119
+ }
120
+ const display = displays[index];
121
+ if (display !== undefined && typeof display !== 'string')
122
+ throw new TypeError('observation_component_invalid');
123
+ return Object.freeze({ code, ...(display ? { display } : {}), value, unit: units[index] });
124
+ }));
125
+ }
126
+ /**
127
+ * Creates generic reduced rows for component-level indexes.
128
+ *
129
+ * These are internal index projections, not independent native FHIR
130
+ * Observations. The missing status deliberately keeps them out of normal
131
+ * top-level Observation results.
132
+ */
133
+ export function materializeObservationComponentIndexEntries(entry) {
134
+ if (isVitalSignObservationGraph(entry)) {
135
+ return Object.freeze(entry.components.map((component, componentIndex) => Object.freeze({
136
+ parentReference: entry.primary.reference,
137
+ componentIndex,
138
+ claims: component.claims,
139
+ })));
140
+ }
141
+ if (entry.resourceType !== 'Observation')
142
+ throw new TypeError('vital_sign_observation_required');
143
+ const subject = required(entry.claims[ObservationClaim.Subject] ?? '', 'vital_sign_subject_required');
144
+ return Object.freeze(parseObservationComponents(entry.claims).map((component, componentIndex) => Object.freeze({
145
+ parentReference: entry.reference,
146
+ componentIndex,
147
+ claims: Object.freeze({
148
+ [ObservationClaim.Subject]: subject,
149
+ [ObservationClaim.Code]: component.code,
150
+ ...(component.display ? { [ObservationClaim.CodeDisplay]: component.display } : {}),
151
+ [ObservationClaim.ValueQuantityNumber]: String(component.value),
152
+ [ObservationClaim.ValueQuantityUnit]: component.unit,
153
+ }),
154
+ })));
155
+ }
156
+ /** Projects the neutral flat contract at the explicit FHIR R4 boundary. */
157
+ export function projectVitalSignObservationR4(entry) {
158
+ return projectVitalSignObservation(entry);
159
+ }
160
+ /** Projects the neutral flat contract at the explicit FHIR R5 boundary. */
161
+ export function projectVitalSignObservationR5(entry) {
162
+ return projectVitalSignObservation(entry);
163
+ }
164
+ function projectVitalSignObservation(source) {
165
+ const entry = isVitalSignObservationGraph(source) ? source.primary : source;
166
+ if (entry.resourceType !== 'Observation')
167
+ throw new TypeError('vital_sign_observation_required');
168
+ const claims = entry.claims;
169
+ const base = {
170
+ resourceType: 'Observation',
171
+ id: required(claims[ObservationClaim.Identifier] ?? '', 'vital_sign_entry_id_required'),
172
+ status: required(claims[ObservationClaim.Status] ?? '', 'vital_sign_status_required'),
173
+ category: [{ coding: [codingFromClaim(required(claims[ObservationClaim.Category] ?? '', 'vital_sign_category_required'))] }],
174
+ code: { coding: [codingFromClaim(required(claims[ObservationClaim.Code] ?? '', 'vital_sign_code_required'), claims[ObservationClaim.CodeDisplay])] },
175
+ subject: { reference: required(claims[ObservationClaim.Subject] ?? '', 'vital_sign_subject_required') },
176
+ effectiveDateTime: required(claims[ObservationClaim.EffectiveDateTime] ?? '', 'vital_sign_measured_at_required'),
177
+ ...(claims[ObservationClaim.Device] ? { device: { reference: claims[ObservationClaim.Device] } } : {}),
178
+ };
179
+ const components = parseObservationComponents(isVitalSignObservationGraph(source) ? source : claims);
180
+ if (components.length) {
181
+ return Object.freeze({ ...base, component: Object.freeze(components.map(component => Object.freeze({
182
+ code: { coding: [codingFromClaim(component.code, component.display)] },
183
+ valueQuantity: quantity(component.value, component.unit),
184
+ }))) });
185
+ }
186
+ const value = Number(claims[ObservationClaim.ValueQuantityNumber]);
187
+ if (!Number.isFinite(value))
188
+ throw new TypeError('vital_sign_value_invalid');
189
+ return Object.freeze({ ...base, valueQuantity: quantity(value, required(claims[ObservationClaim.ValueQuantityUnit] ?? '', 'vital_sign_unit_required')) });
190
+ }
191
+ function codingFromClaim(value, display) {
192
+ const separator = value.lastIndexOf('|');
193
+ return separator < 0
194
+ ? Object.freeze({ code: value, ...(display ? { display } : {}) })
195
+ : Object.freeze({ system: value.slice(0, separator), code: value.slice(separator + 1), ...(display ? { display } : {}) });
196
+ }
197
+ function quantity(value, unitClaim) {
198
+ const coding = codingFromClaim(unitClaim);
199
+ return Object.freeze({ value, unit: coding.code, ...coding });
200
+ }
201
+ /** Converts a device/API batch to independent preliminary Observations without aggregation. */
202
+ export function normalizeVitalSignDeviceBatch(input) {
203
+ const seen = new Set();
204
+ return Object.freeze(input.readings.map(reading => {
205
+ if (seen.has(reading.entryId))
206
+ throw new TypeError('vital_sign_entry_id_duplicate');
207
+ seen.add(reading.entryId);
208
+ return buildVitalSignObservation({
209
+ ...reading,
210
+ subjectReference: input.subjectReference,
211
+ deviceReference: input.deviceReference,
212
+ status: 'preliminary',
213
+ });
214
+ }));
215
+ }
216
+ function buildBloodPressureComponent(input, role, value, coding) {
217
+ const entryId = `${required(input.entryId, 'vital_sign_entry_id_required')}-${role}`;
218
+ const claims = {
219
+ [ObservationClaim.Identifier]: entryId,
220
+ [ObservationClaim.Subject]: required(input.subjectReference, 'vital_sign_subject_required'),
221
+ [ObservationClaim.Category]: ObservationCategoryCodes.VitalSigns.claim,
222
+ [ObservationClaim.Code]: coding.claim,
223
+ [ObservationClaim.ValueQuantityNumber]: String(value),
224
+ [ObservationClaim.ValueQuantityUnit]: VitalSignUnit.bloodPressure,
225
+ [ObservationClaim.EffectiveDateTime]: normalizeDateTime(input.measuredAt),
226
+ };
227
+ if (coding.display)
228
+ claims[ObservationClaim.CodeDisplay] = coding.display;
229
+ if (input.deviceReference)
230
+ claims[ObservationClaim.Device] = required(input.deviceReference, 'vital_sign_device_required');
231
+ return buildFlatClaimResourceEntry({ entryId, resourceType: 'Observation', claims });
232
+ }
233
+ function freezeObservationGraph(primary, components) {
234
+ const frozenComponents = Object.freeze([...components]);
235
+ return Object.freeze({ primary, components: frozenComponents, entries: Object.freeze([primary, ...frozenComponents]) });
236
+ }
237
+ function isVitalSignObservationGraph(source) {
238
+ return 'primary' in source && 'components' in source && 'entries' in source;
239
+ }
240
+ function componentQuantityFromEntry(entry) {
241
+ if (entry.resourceType !== 'Observation')
242
+ throw new TypeError('observation_component_invalid');
243
+ const value = Number(entry.claims[ObservationClaim.ValueQuantityNumber]);
244
+ const code = entry.claims[ObservationClaim.Code];
245
+ const unit = entry.claims[ObservationClaim.ValueQuantityUnit];
246
+ if (!code || !unit || !Number.isFinite(value))
247
+ throw new TypeError('observation_component_invalid');
248
+ const display = entry.claims[ObservationClaim.CodeDisplay];
249
+ return Object.freeze({ code, ...(display ? { display } : {}), value, unit });
250
+ }
251
+ function scalarDefinition(kind) {
252
+ switch (kind) {
253
+ case 'temperature': return { coding: VitalSignsCodes.BodyTemperature, unit: VitalSignUnit.temperature };
254
+ case 'heart-rate': return { coding: VitalSignsCodes.HeartRate, unit: VitalSignUnit.heartRate };
255
+ case 'oxygen-saturation': return { coding: VitalSignsCodes.OxygenSaturation, unit: VitalSignUnit.oxygenSaturation };
256
+ case 'respiratory-rate': return { coding: VitalSignsCodes.RespiratoryRate, unit: VitalSignUnit.respiratoryRate };
257
+ case 'body-weight': return { coding: VitalSignsCodes.BodyWeight, unit: VitalSignUnit.bodyWeight };
258
+ }
259
+ }
260
+ function normalizeDateTime(value) {
261
+ const date = new Date(value);
262
+ if (!value.trim() || Number.isNaN(date.valueOf()))
263
+ throw new TypeError('vital_sign_measured_at_invalid');
264
+ return date.toISOString();
265
+ }
266
+ function validPositive(value) {
267
+ return Number.isFinite(value) && value > 0;
268
+ }
269
+ function required(value, error) {
270
+ const normalized = value.trim();
271
+ if (!normalized || normalized.includes(','))
272
+ throw new TypeError(error);
273
+ return normalized;
274
+ }
275
+ /** Reusable neutral fixtures; consumers and tests must not duplicate wire examples. */
276
+ export const VITAL_SIGN_EXAMPLES = Object.freeze({
277
+ subjectReference: 'Patient/example-subject',
278
+ deviceReference: 'Device/example-home-monitor',
279
+ bloodPressure: Object.freeze({ entryId: 'bp-example-1', measuredAt: '2026-09-21T08:15:00-07:00', systolic: 121, diastolic: 79 }),
280
+ heartRateBatch: Object.freeze([
281
+ Object.freeze({ entryId: 'hr-example-1', kind: 'heart-rate', measuredAt: '2026-09-20T08:00:00Z', value: 68 }),
282
+ Object.freeze({ entryId: 'hr-example-2', kind: 'heart-rate', measuredAt: '2026-09-21T08:00:00Z', value: 72 }),
283
+ ]),
284
+ });
@@ -0,0 +1,72 @@
1
+ # UHC FHIR utilities migration
2
+
3
+ `uhc-fhir-utils-typescript` is an archaeological source, not a dependency of
4
+ this package. Its useful behavior must be recovered behind neutral, typed and
5
+ version-explicit contracts. Its product branding, obsolete security choices
6
+ and transport assumptions must not be copied.
7
+
8
+ ## Migration rules
9
+
10
+ 1. Add the smallest failing contract test here before porting behavior.
11
+ 2. Use official FHIR R4/R5/R6 structures at import/export boundaries and flat
12
+ claims as the version-neutral representation between those boundaries.
13
+ 3. Copy no raw claim, terminology, identifier or profile literal into a
14
+ consumer. Define it once in its owning neutral or sector data package.
15
+ 4. Preserve historical inputs with explicit read adapters. New writers emit
16
+ only the current canonical representation.
17
+ 5. Do not change `gdc-*` while those repositories are frozen. They are
18
+ read-only compatibility evidence until every consumer uses this package.
19
+
20
+ ## Reuse, rewrite or retire
21
+
22
+ | Old area | Decision | Current destination or gate |
23
+ | --- | --- | --- |
24
+ | `Bundle` and `Composition` traversal | Rewrite from tests | Neutral graph and explicit R4/R5/R6 adapters; preserve author, attester and section boundaries |
25
+ | `CodeableConcept`, `Identifier`, `Reference`, `Quantity` | Rewrite and reuse behavior | Small typed value modules with round-trip tests |
26
+ | `Observation` and resource parameter models | Rewrite | Canonical claim catalogs plus version-specific projection adapters |
27
+ | `Parameters` conversion | Rewrite | A typed FHIR Parameters module; never a transport-specific envelope |
28
+ | Anonymization field inventory | Test input only | A separately reviewed policy contract; old field lists do not become policy automatically |
29
+ | ATC, LOINC, SNOMED and EMA JSON | Validate before reuse | Terminology service fixtures, never authoritative embedded catalogs |
30
+ | DICOM parsing | Extract later | Separate imaging package; do not add `dicom-parser` to the neutral core |
31
+ | SHA-1 attachment hashing | Retire | No compatibility writer; historical read/verification belongs in a versioned adapter |
32
+ | `MessageHeader` transport conventions | Retire | Transport is outside the FHIR data contract |
33
+ | UHC-branded templates and defaults | Retire or move downstream | Product policy belongs in UHC packages, not here |
34
+ | Babel/Yarn/Jest build plumbing and broad `any` models | Retire | Node 24, TypeScript strict mode and `node:test` |
35
+
36
+ ## Observation and component contract
37
+
38
+ FHIR blood pressure is one native `Observation` with systolic and diastolic
39
+ entries in `Observation.component[]`. Canonical flat storage is instead a
40
+ resource graph: the primary Observation owns `Observation.has-member`, and
41
+ each referenced reduced Observation owns its identifier, subject, category,
42
+ date, component code, numeric value and unit. Reduced entries deliberately
43
+ carry no `Observation.status`, so normal top-level listings ignore them while
44
+ component-level indexes can query their ordinary Observation claims.
45
+
46
+ The R4/R5 export adapter resolves the primary entry's members and reconstructs
47
+ one native `Observation.component[]`; it does not export three independent
48
+ native Observations. Component entries do not use `is-contained`,
49
+ `contained-parent-reference` or `contained-reference-list`.
50
+
51
+ Aligned JSON arrays and the older comma-separated `Observation.component-*`
52
+ claims are accepted only by the migration reader. No current writer emits
53
+ either representation.
54
+
55
+ The historical `Observation.bp-systolic-number` and
56
+ `Observation.bp-diastolic-number` fields are accepted only when reading data
57
+ that has no component graph or historical component claims. New writers and
58
+ generic component indexes never emit them. This prevents a new custom claim
59
+ family for every assessment scale.
60
+
61
+ ## Remaining migration sequence
62
+
63
+ 1. Inventory imports from `gdc-common-utils-ts` in SOS, Vet, UHC, portals and
64
+ assistants by resource family.
65
+ 2. Move one neutral resource family at a time into this package with exact
66
+ compatibility vectors and R4/R5 projection tests.
67
+ 3. Release this package, pin the immutable version in the lowest downstream
68
+ package, and run that consumer's complete local matrix.
69
+ 4. Remove the corresponding frozen dependency only after no consumer imports
70
+ that family from it.
71
+ 5. Migrate imaging/DICOM and terminology payloads as separate changes because
72
+ they have different dependencies, security rules and release gates.
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "fhir-data-utils-ts",
3
+ "version": "0.2.4",
4
+ "description": "FHIR-version-neutral flat-claim and SearchParameter data utilities",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./coding-review-flat-claims": {
15
+ "types": "./dist/coding-review-flat-claims.d.ts",
16
+ "default": "./dist/coding-review-flat-claims.js"
17
+ },
18
+ "./flat-claim-resource-graph": {
19
+ "types": "./dist/flat-claim-resource-graph.d.ts",
20
+ "default": "./dist/flat-claim-resource-graph.js"
21
+ },
22
+ "./search-parameters": {
23
+ "types": "./dist/search-parameters.d.ts",
24
+ "default": "./dist/search-parameters.js"
25
+ },
26
+ "./observation-claims": {
27
+ "types": "./dist/observation-claims.d.ts",
28
+ "default": "./dist/observation-claims.js"
29
+ },
30
+ "./vital-sign-observations": {
31
+ "types": "./dist/vital-sign-observations.d.ts",
32
+ "default": "./dist/vital-sign-observations.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist",
37
+ ".codex/skills",
38
+ "docs",
39
+ "README.md",
40
+ "CHANGELOG.md"
41
+ ],
42
+ "scripts": {
43
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
44
+ "build": "npm run clean && tsc -p tsconfig.json",
45
+ "typecheck": "tsc -p tsconfig.json --noEmit",
46
+ "test": "npm run build && node --test tests/*.test.mjs",
47
+ "check": "npm run typecheck && npm test",
48
+ "prepack": "npm run check"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^24.10.0",
52
+ "typescript": "^5.9.3"
53
+ },
54
+ "engines": {
55
+ "node": ">=24"
56
+ },
57
+ "private": false,
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
61
+ }