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.
- package/.codex/skills/govern-fhir-observation-components/SKILL.md +74 -0
- package/.codex/skills/govern-fhir-observation-components/agents/openai.yaml +4 -0
- package/.codex/skills/preserve-fhir-flat-coding-review/SKILL.md +57 -0
- package/.codex/skills/preserve-fhir-flat-coding-review/agents/openai.yaml +4 -0
- package/CHANGELOG.md +44 -0
- package/README.md +72 -0
- package/dist/coding-review-flat-claims.d.ts +134 -0
- package/dist/coding-review-flat-claims.js +160 -0
- package/dist/flat-claim-resource-graph.d.ts +48 -0
- package/dist/flat-claim-resource-graph.js +108 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/observation-claims.d.ts +132 -0
- package/dist/observation-claims.js +96 -0
- package/dist/search-parameters.d.ts +18 -0
- package/dist/search-parameters.js +34 -0
- package/dist/vital-sign-observations.d.ts +250 -0
- package/dist/vital-sign-observations.js +284 -0
- package/docs/UHC_FHIR_UTILS_MIGRATION.md +72 -0
- package/package.json +61 -0
|
@@ -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
|
+
}
|