fhir-data-utils-ts 0.3.7 → 0.6.0

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.
Files changed (79) hide show
  1. package/.codex/skills/govern-fhir-data-utilities/SKILL.md +35 -0
  2. package/.codex/skills/govern-fhir-observation-components/SKILL.md +11 -0
  3. package/CHANGELOG.md +63 -0
  4. package/README.md +44 -0
  5. package/dist/appointment-communication-test-data.d.ts +63 -0
  6. package/dist/appointment-communication-test-data.js +77 -0
  7. package/dist/builders/department-service-graph.d.ts +32 -0
  8. package/dist/builders/department-service-graph.js +102 -0
  9. package/dist/builders/device-compliance-graph.d.ts +51 -0
  10. package/dist/builders/device-compliance-graph.js +115 -0
  11. package/dist/builders/index.d.ts +3 -0
  12. package/dist/builders/index.js +3 -0
  13. package/dist/builders/service-schedule-graph.d.ts +58 -0
  14. package/dist/builders/service-schedule-graph.js +149 -0
  15. package/dist/claims/appointment-claims.d.ts +6 -0
  16. package/dist/claims/appointment-claims.js +6 -0
  17. package/dist/claims/composition-claims.d.ts +1 -0
  18. package/dist/claims/composition-claims.js +1 -0
  19. package/dist/claims/device-metric-claims.d.ts +16 -0
  20. package/dist/claims/device-metric-claims.js +25 -0
  21. package/dist/claims/healthcare-service-claims.d.ts +32 -0
  22. package/dist/claims/healthcare-service-claims.js +54 -0
  23. package/dist/claims/index.d.ts +4 -0
  24. package/dist/claims/index.js +4 -0
  25. package/dist/claims/location-claims.d.ts +3 -0
  26. package/dist/claims/location-claims.js +6 -0
  27. package/dist/claims/organization-claims.d.ts +3 -1
  28. package/dist/claims/organization-claims.js +4 -1
  29. package/dist/claims/procedure-claims.d.ts +2 -0
  30. package/dist/claims/procedure-claims.js +4 -0
  31. package/dist/claims/regulated-authorization-claims.d.ts +17 -0
  32. package/dist/claims/regulated-authorization-claims.js +27 -0
  33. package/dist/claims/schedule-slot-claims.d.ts +45 -0
  34. package/dist/claims/schedule-slot-claims.js +70 -0
  35. package/dist/constants/fhir-resource-types.d.ts +9 -0
  36. package/dist/constants/fhir-resource-types.js +3 -0
  37. package/dist/converters/convert-appointment-communication.d.ts +14 -0
  38. package/dist/converters/convert-appointment-communication.js +97 -0
  39. package/dist/converters/convert-appointment.js +16 -7
  40. package/dist/converters/convert-composition.js +12 -2
  41. package/dist/converters/convert-device-metric.d.ts +3 -0
  42. package/dist/converters/convert-device-metric.js +37 -0
  43. package/dist/converters/convert-document-reference.js +5 -0
  44. package/dist/converters/convert-healthcare-service.d.ts +7 -0
  45. package/dist/converters/convert-healthcare-service.js +60 -0
  46. package/dist/converters/convert-location.d.ts +4 -0
  47. package/dist/converters/convert-location.js +41 -3
  48. package/dist/converters/convert-organization.d.ts +2 -0
  49. package/dist/converters/convert-organization.js +20 -3
  50. package/dist/converters/convert-procedure.d.ts +2 -0
  51. package/dist/converters/convert-procedure.js +21 -0
  52. package/dist/converters/convert-regulated-authorization.d.ts +3 -0
  53. package/dist/converters/convert-regulated-authorization.js +34 -0
  54. package/dist/converters/convert-schedule-slot.d.ts +9 -0
  55. package/dist/converters/convert-schedule-slot.js +96 -0
  56. package/dist/converters/convert-shared.d.ts +10 -0
  57. package/dist/converters/convert-shared.js +19 -0
  58. package/dist/converters/index.d.ts +6 -0
  59. package/dist/converters/index.js +6 -0
  60. package/dist/converters/materialize-claims-first-resource.d.ts +16 -0
  61. package/dist/converters/materialize-claims-first-resource.js +61 -0
  62. package/dist/fhir-bundle.d.ts +21 -0
  63. package/dist/fhir-bundle.js +74 -0
  64. package/dist/group-r5.d.ts +12 -0
  65. package/dist/group-r5.js +20 -0
  66. package/dist/healthcare-service-specialty-map.d.ts +32 -0
  67. package/dist/healthcare-service-specialty-map.js +151 -0
  68. package/dist/index.d.ts +2 -0
  69. package/dist/index.js +2 -0
  70. package/dist/observation-claims.d.ts +3 -0
  71. package/dist/observation-claims.js +3 -0
  72. package/dist/vital-sign-observations.d.ts +75 -1
  73. package/dist/vital-sign-observations.js +225 -9
  74. package/docs/CLINICAL_TYPED_ACCESSOR_BOUNDARY.md +44 -0
  75. package/docs/COMMUNICATION_BUNDLE_CONTRACT.md +61 -0
  76. package/docs/HEALTHCARE_SERVICE_DEPARTMENT_CATALOG.md +190 -0
  77. package/docs/UHC_FHIR_UTILS_MIGRATION.md +20 -1
  78. package/docs/VITAL_SIGN_DAILY_DOCUMENT.md +79 -0
  79. package/package.json +5 -1
@@ -29,10 +29,45 @@ description: Complete or audit migrations, claim catalogs, converters, builders,
29
29
  projection. Do not invent resource-specific scalar aliases for indexing.
30
30
  - Keep Group membership and ResearchStudy associated parties descriptive;
31
31
  neither is a Consent, SMART scope or authorization grant.
32
+ - Keep legal Organizations, department Organizations, HealthcareServices and
33
+ Locations distinct. A professional Group has one department managingEntity;
34
+ endpoint routing and access remain downstream policy.
35
+ - Build a department and all independently schedulable offered services with
36
+ `buildDepartmentServicesFlatGraph(...)`; require complete identifier and
37
+ coding tokens plus typed references, and persist one Organization plus one
38
+ claims-first HealthcareService row per service.
39
+ - Keep `Schedule` and `Slot` as independent claims-first resources. A Schedule
40
+ names its HealthcareService plus all independently required Location, Device
41
+ or professional actors; every Slot references its Schedule and derives
42
+ duration only from required start/end instants.
43
+ - Model rooms, booths, chairs and beds as child Locations using `partOf`.
44
+ Model equipment as Device, never as a synthetic Service or Place. R4
45
+ `Location.physicalType` and R5 `Location.form` project the same governed
46
+ version-neutral physical-form claim.
47
+ - Keep equipment calibration and public authorization separate. A calibration
48
+ is a Procedure focused on the Device, a DeviceMetric calibration state/time
49
+ and optional DocumentReference evidence. A public operating approval is an
50
+ R5 RegulatedAuthorization whose holder and regulator are distinct. Neither
51
+ record grants the submitter authority, and neither is collapsed into the
52
+ Schedule or a browser-authored availability flag.
53
+ - Treat minimal identity plus `resource.meta.claims` as the only canonical
54
+ persisted resource representation. R4/R5 materializers keep each department
55
+ and service id independent; the native result is an adapter output for
56
+ interop, hashing or credentials, never a parallel stored source of truth.
57
+ - Preserve terminology-map relationship evidence. Only a strict reviewed
58
+ equivalence may be selected automatically; wider, related, inexact and
59
+ unmatched mappings require terminology governance.
32
60
  - Keep native R5 SubscriptionTopic/Subscription configuration and notification
33
61
  Bundle parsing separate from the claims-first Communication being delivered.
34
62
  This package owns their reusable types and validation, never HTTP delivery,
35
63
  subscriber persistence, authentication or product notification policy.
64
+ - Validate and project the canonical outer `Bundle[type=batch]` ->
65
+ `Communication` -> inner `Bundle[type=document|batch|collection]` structure
66
+ here, but keep the cross-product high-level editor/reader in `sos-sdk-core`
67
+ and its data contracts in `sos-data-utils`. Neither replacement package may
68
+ import frozen `gdc-*`. Arbitrary files are DocumentReference entries in the
69
+ inner Bundle, never a second direct Communication attachment. Direct
70
+ flat-claim CRUD is an internal persistence boundary, not this wire format.
36
71
  - Keep ImagingStudy searchable metadata, clinical references and protected
37
72
  endpoint references in neutral flat claims. DICOM parsing, pixels and object
38
73
  storage remain in a separately versioned imaging/storage package.
@@ -53,6 +53,17 @@ description: Implement or audit neutral FHIR Observation claim catalogs, vital-s
53
53
  copy `Observation.*`, LOINC, UCUM or category strings into portals,
54
54
  assistants, tests or snippets.
55
55
  - Keep displays separate from `system|code` tokens and local-language text.
56
+ - For repeated vital signs, keep every raw reading as `registered`, require the
57
+ configured cadence before deriving a `preliminary` mean, and preserve the
58
+ source links through `Observation.derived-from`.
59
+ - Persist the EventTiming bucket through the canonical
60
+ `Observation.date-when` claim. Do not infer MORN/EVE from a server time zone.
61
+ - Build one preliminary Composition per explicit local date and IANA time
62
+ zone. Only authorized confirmation returns a final daily document with a
63
+ professional attester and final top-level Observations.
64
+ - Keep ledger hashing, individual-index writes, Digital Twin materialization
65
+ and research eligibility outside this neutral package. A final graph is data
66
+ state, not authorization for those side effects.
56
67
 
57
68
  ## Change with TDD
58
69
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.6.0 - 2026-09-29
6
+
7
+ - Add canonical two-level Communication Bundle validation tools recovered from
8
+ the frozen shared implementation: outer batch Bundle, one FHIR Bundle
9
+ attachment per Communication, and validated document/batch/collection inner
10
+ Bundles. Reject direct clinical or binary Communication payloads while
11
+ leaving high-level mutable editors to `sos-sdk-core`.
12
+ - Add a typed Appointment exchange projection that preserves start/end,
13
+ participants, slots and supporting information while keeping an ICS calendar
14
+ file as an independent DocumentReference flat-claim resource.
15
+
16
+ ## 0.5.0 - 2026-09-29
17
+
18
+ - Add claims-first equipment compliance graphs: technical calibration as a
19
+ Device-focused Procedure plus DeviceMetric state and optional hashed
20
+ DocumentReference evidence, and public operating approval as a separate R5
21
+ RegulatedAuthorization. Keep these records independent from Schedule
22
+ availability and downstream role authorization.
23
+
24
+ - Add official R5 Schedule and Slot search-claim catalogs, claims-first
25
+ materializers and a service availability graph builder. Preserve independent
26
+ HealthcareService/Location actors and derive booking duration from each
27
+ Slot start/end interval.
28
+ - Add schedulable child Location construction and composite Schedule actors for
29
+ Location, Device and PractitionerRole capacity. Correct R5 Location
30
+ projection to use `form` instead of the removed R4 `physicalType` field.
31
+ - Add a strict claims-first department/service graph builder that emits a
32
+ department Organization and one independently schedulable HealthcareService
33
+ per offered service, with complete identifiers, coding tokens and Location
34
+ references. Add governed
35
+ R5 Organization description and HealthcareService comment projection without
36
+ presenting either as an R4 SearchParameter.
37
+
38
+ - Added strict claims-first R5 materialization for Organization,
39
+ HealthcareService and Location. Canonical `system|value` identifiers and
40
+ `system|code` codings now retain their systems, while nested native FHIR
41
+ beside `resource.meta.claims` is rejected.
42
+
43
+ - Document the ownership boundary for allergy reaction severity, medication
44
+ treatment period, immunization route/site and Observation reference-range
45
+ text across neutral FHIR projections, SOS multichannel schemas and typed SDK
46
+ Bundle editors.
47
+
48
+ - Add the complete official FHIR R4 HealthcareService search-claim catalog and
49
+ a lossless R4 converter that keeps provider departments and services distinct.
50
+ - Preserve Location WGS84 coordinates and require each professional service
51
+ Group to have one managing department Organization without granting access.
52
+ - Add the supplied 115-row SNOMED CT C80 to LOINC specialty candidate map;
53
+ only strict equivalence is eligible for automatic selection.
54
+
55
+ ## 0.4.0 - 2026-09-27
56
+
57
+ - Add the neutral daily vital-sign document lifecycle: registered raw readings,
58
+ preliminary three-reading means, MORN/EVE EventTiming, one preliminary daily
59
+ Composition and explicit professional finalization.
60
+ - Preserve mean provenance through `Observation.derived-from`, validate the
61
+ local calendar day with an IANA time zone and project Composition status plus
62
+ section entry references at the FHIR R4 boundary.
63
+ - Add governed UCUM Fahrenheit and avoirdupois-pound input alongside Celsius
64
+ and kilograms without moving regional UI preferences into shared FHIR data.
65
+
3
66
  ## 0.3.7 - 2026-09-26
4
67
 
5
68
  - Add claims-first FHIR contracts for Location-linked Encounter care journeys,
package/README.md CHANGED
@@ -36,12 +36,37 @@ independent `DocumentReference`. Preliminary reports can be reviewed and
36
36
  securely exchanged; ledger and research eligibility remain downstream policy
37
37
  and normally require the confirmed final lifecycle state.
38
38
 
39
+ Provider catalogs keep legal Organizations, department Organizations,
40
+ HealthcareServices and Locations separate. A department uses
41
+ `Organization.part-of`; each offered service references its provider through
42
+ `HealthcareService.organization`, and may reference several geocoded
43
+ Locations. Professional Groups are bound to one department without turning
44
+ membership into authorization. See
45
+ [Organization departments and healthcare-service catalogs](docs/HEALTHCARE_SERVICE_DEPARTMENT_CATALOG.md).
46
+
47
+ Cross-participant data always uses an outer FHIR batch Bundle containing
48
+ Communication entries. Each Communication carries exactly one
49
+ `application/fhir+json` inner document, batch or collection Bundle; arbitrary
50
+ files are DocumentReference resources inside that Bundle. This package owns
51
+ strict parsing and projection tools; the shared high-level editor/reader belongs
52
+ to `sos-sdk-core`, with cross-sector contracts in `sos-data-utils`. See
53
+ [Communication Bundle contract](docs/COMMUNICATION_BUNDLE_CONTRACT.md).
54
+
55
+ Reservable equipment remains a `Device` actor of its service-specific
56
+ `Schedule`. Technical calibration is recorded as a `Procedure` focused on that
57
+ Device plus the current `DeviceMetric.calibration` state and an optional
58
+ `DocumentReference` evidence hash. A public operating approval is a separate
59
+ R5 `RegulatedAuthorization`; it is never inferred from calibration or from an
60
+ employee role.
61
+
39
62
  ## Public contract
40
63
 
41
64
  The root export and stable subpaths provide:
42
65
 
43
66
  - strict version-neutral FHIR datatypes, value/token helpers, Bundle traversal
44
67
  and typed search parsing/building;
68
+ - the canonical Communication Bundle validation boundary and Appointment
69
+ exchange projection, including calendar DocumentReference evidence;
45
70
  - complete flat-claim catalogs and claim helpers for the resource families
46
71
  formerly scattered through `gdc-common-utils-ts`;
47
72
  - explicit R4/R5 conversion adapters, including Consent and PractitionerRole;
@@ -72,6 +97,11 @@ Use `fhir-data-utils-ts/claims`, `/converters`, `/fhir-bundle`,
72
97
  `/fhir-search-parameters`, `/clinical-terminology`, `/group-r5` or
73
98
  `/research-study-r5` as appropriate.
74
99
 
100
+ The `/healthcare-service-specialty-map` subpath exposes the complete supplied
101
+ C80-to-LOINC candidate mapping. Only strict equivalence can be selected
102
+ automatically; wider, related, inexact and unmatched candidates remain review
103
+ evidence.
104
+
75
105
  Repeatable `CodeableConcept.coding[]` identities are serialized in the owning
76
106
  token claim as a comma-separated list of complete `system|code` tokens. The
77
107
  first `*-display` remains source evidence for compatibility; authoritative
@@ -105,10 +135,24 @@ without inventing a new flat-claim family for each scale.
105
135
  `Observation.user-selected` provenance flag. Consumers submit its `entries`
106
136
  without reconstructing component claims or relationships.
107
137
 
138
+ Repeated morning/evening capture uses `buildRepeatedVitalSignAverage`: three
139
+ two-minute raw readings remain `registered`, while their derived arithmetic
140
+ mean is `preliminary`. `buildVitalSignDailyDocument` groups the day's top-level
141
+ Observations under one preliminary Composition; only
142
+ `finalizeVitalSignDailyDocument` returns a final Composition and final
143
+ top-level Observations with a professional attester. Ledger hashing, index
144
+ updates, Digital Twin and research eligibility remain owning-service policy.
145
+ See [Daily vital-sign document lifecycle](docs/VITAL_SIGN_DAILY_DOCUMENT.md).
146
+
108
147
  See [the UHC FHIR utilities migration](docs/UHC_FHIR_UTILS_MIGRATION.md) for the
109
148
  reuse/rewrite/retire inventory and the rule for moving frozen contracts without
110
149
  changing any `gdc-*` repository.
111
150
 
151
+ See [Clinical typed accessor boundary](docs/CLINICAL_TYPED_ACCESSOR_BOUNDARY.md)
152
+ for the exact division between canonical clinical claims/converters here,
153
+ shared SOS multichannel schemas and the mutable Bundle getters/setters exposed
154
+ by runtime SDKs.
155
+
112
156
  ## Coding review for imported clinical text
113
157
 
114
158
  DataConv, Vet and UHC share one neutral review contract from
@@ -0,0 +1,63 @@
1
+ import type { FhirBundle, FhirResource } from './fhir-types.js';
2
+ /** Repository-safe reusable evidence for appointment Bundle and calendar tests. */
3
+ export declare const FHIR_APPOINTMENT_COMMUNICATION_TEST_DATA: Readonly<{
4
+ release: "r5";
5
+ start: "2026-10-05T16:00:00.000Z";
6
+ end: "2026-10-05T16:30:00.000Z";
7
+ communicationId: "communication-one";
8
+ subjectReference: "Patient/animal-one";
9
+ senderReference: "Organization/clinic-one";
10
+ recipientReference: "RelatedPerson/controller-one";
11
+ appointmentReference: "Appointment/appointment-one";
12
+ calendarData: string;
13
+ appointment: Readonly<{
14
+ [key: string]: unknown;
15
+ resourceType: string;
16
+ id?: string;
17
+ language?: string;
18
+ meta?: Readonly<Record<string, unknown>>;
19
+ contained?: readonly FhirResource[];
20
+ }>;
21
+ calendarDocument: Readonly<{
22
+ [key: string]: unknown;
23
+ resourceType: string;
24
+ id?: string;
25
+ language?: string;
26
+ meta?: Readonly<Record<string, unknown>>;
27
+ contained?: readonly FhirResource[];
28
+ }>;
29
+ communication: Readonly<{
30
+ [key: string]: unknown;
31
+ resourceType: string;
32
+ id?: string;
33
+ language?: string;
34
+ meta?: Readonly<Record<string, unknown>>;
35
+ contained?: readonly FhirResource[];
36
+ }>;
37
+ outerBatch: FhirBundle;
38
+ outerBatchWithInvalidDocument: FhirBundle;
39
+ outerBatchWithInvalidInnerBatch: FhirBundle;
40
+ outerBatchWithBareAppointment: Readonly<{
41
+ resourceType: "Bundle";
42
+ type: "batch";
43
+ entry: {
44
+ resource: {
45
+ payload: {
46
+ contentAttachment: {
47
+ contentType: string;
48
+ data: string;
49
+ };
50
+ }[];
51
+ resourceType: string;
52
+ id?: string;
53
+ language?: string;
54
+ meta?: Readonly<Record<string, unknown>>;
55
+ contained?: readonly FhirResource[];
56
+ };
57
+ request: {
58
+ method: string;
59
+ url: string;
60
+ };
61
+ }[];
62
+ }>;
63
+ }>;
@@ -0,0 +1,77 @@
1
+ const APPOINTMENT_ID = 'appointment-one';
2
+ const COMMUNICATION_ID = 'communication-one';
3
+ const CALENDAR_DOCUMENT_ID = 'appointment-calendar-one';
4
+ const SUBJECT_REFERENCE = 'Patient/animal-one';
5
+ const SENDER_REFERENCE = 'Organization/clinic-one';
6
+ const RECIPIENT_REFERENCE = 'RelatedPerson/controller-one';
7
+ const APPOINTMENT_REFERENCE = `Appointment/${APPOINTMENT_ID}`;
8
+ const CALENDAR_DOCUMENT_REFERENCE = `DocumentReference/${CALENDAR_DOCUMENT_ID}`;
9
+ const APPOINTMENT_START = '2026-10-05T16:00:00.000Z';
10
+ const APPOINTMENT_END = '2026-10-05T16:30:00.000Z';
11
+ const CALENDAR_DATA = encodeBase64Utf8('BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nDTSTART:20261005T160000Z\r\nDTEND:20261005T163000Z\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n');
12
+ const APPOINTMENT = Object.freeze({
13
+ resourceType: 'Appointment', id: APPOINTMENT_ID, identifier: [{ value: APPOINTMENT_ID }], status: 'booked',
14
+ start: APPOINTMENT_START, end: APPOINTMENT_END,
15
+ participant: [{ actor: { reference: SUBJECT_REFERENCE }, status: 'accepted' }, { actor: { reference: 'PractitionerRole/veterinarian-one' }, status: 'accepted' }, { actor: { reference: 'Location/consultation-one' }, status: 'accepted' }],
16
+ slot: [{ reference: 'Slot/consultation-one' }], supportingInformation: [{ reference: CALENDAR_DOCUMENT_REFERENCE }],
17
+ });
18
+ const CALENDAR_DOCUMENT = Object.freeze({
19
+ resourceType: 'DocumentReference', id: CALENDAR_DOCUMENT_ID, identifier: [{ value: CALENDAR_DOCUMENT_ID }], status: 'current',
20
+ subject: { reference: SUBJECT_REFERENCE }, context: { related: [{ reference: APPOINTMENT_REFERENCE }] },
21
+ content: [{ attachment: { contentType: 'text/calendar', data: CALENDAR_DATA, title: 'appointment.ics' } }],
22
+ });
23
+ const INNER_BUNDLE = Object.freeze({ resourceType: 'Bundle', type: 'collection', entry: [{ resource: APPOINTMENT }, { resource: CALENDAR_DOCUMENT }] });
24
+ const COMMUNICATION = Object.freeze({
25
+ resourceType: 'Communication', id: COMMUNICATION_ID, identifier: [{ value: COMMUNICATION_ID }], status: 'completed',
26
+ subject: { reference: SUBJECT_REFERENCE }, recipient: [{ reference: RECIPIENT_REFERENCE }], sender: { reference: SENDER_REFERENCE },
27
+ payload: [{ contentAttachment: { contentType: 'application/fhir+json', data: encodeBase64Utf8(JSON.stringify(INNER_BUNDLE)), title: 'appointment-bundle.json' } }],
28
+ });
29
+ function communicationWithInnerBundle(innerBundle) {
30
+ return Object.freeze({
31
+ ...COMMUNICATION,
32
+ payload: [{ contentAttachment: { contentType: 'application/fhir+json', data: encodeBase64Utf8(JSON.stringify(innerBundle)) } }],
33
+ });
34
+ }
35
+ function outerBatchWithCommunication(communication) {
36
+ return Object.freeze({
37
+ resourceType: 'Bundle',
38
+ type: 'batch',
39
+ entry: [{ resource: communication, request: { method: 'POST', url: 'Communication' } }],
40
+ });
41
+ }
42
+ /** Repository-safe reusable evidence for appointment Bundle and calendar tests. */
43
+ export const FHIR_APPOINTMENT_COMMUNICATION_TEST_DATA = Object.freeze({
44
+ release: 'r5',
45
+ start: APPOINTMENT_START,
46
+ end: APPOINTMENT_END,
47
+ communicationId: COMMUNICATION_ID,
48
+ subjectReference: SUBJECT_REFERENCE,
49
+ senderReference: SENDER_REFERENCE,
50
+ recipientReference: RECIPIENT_REFERENCE,
51
+ appointmentReference: APPOINTMENT_REFERENCE,
52
+ calendarData: CALENDAR_DATA,
53
+ appointment: APPOINTMENT,
54
+ calendarDocument: CALENDAR_DOCUMENT,
55
+ communication: COMMUNICATION,
56
+ outerBatch: outerBatchWithCommunication(COMMUNICATION),
57
+ outerBatchWithInvalidDocument: outerBatchWithCommunication(communicationWithInnerBundle(Object.freeze({
58
+ resourceType: 'Bundle',
59
+ type: 'document',
60
+ entry: [{ resource: APPOINTMENT }],
61
+ }))),
62
+ outerBatchWithInvalidInnerBatch: outerBatchWithCommunication(communicationWithInnerBundle(Object.freeze({
63
+ resourceType: 'Bundle',
64
+ type: 'batch',
65
+ entry: [{ resource: APPOINTMENT }],
66
+ }))),
67
+ outerBatchWithBareAppointment: Object.freeze({
68
+ resourceType: 'Bundle', type: 'batch', entry: [{ resource: { ...COMMUNICATION, payload: [{ contentAttachment: { contentType: 'application/fhir+json', data: encodeBase64Utf8(JSON.stringify(APPOINTMENT)) } }] }, request: { method: 'POST', url: 'Communication' } }],
69
+ }),
70
+ });
71
+ function encodeBase64Utf8(value) {
72
+ const bytes = new TextEncoder().encode(value);
73
+ let binary = '';
74
+ for (const byte of bytes)
75
+ binary += String.fromCharCode(byte);
76
+ return globalThis.btoa(binary);
77
+ }
@@ -0,0 +1,32 @@
1
+ export type ClaimsFirstGraphResource = Readonly<{
2
+ resourceType: 'Organization' | 'HealthcareService';
3
+ id: string;
4
+ meta: Readonly<{
5
+ claims: Readonly<Record<string, string | readonly string[]>>;
6
+ }>;
7
+ }>;
8
+ export type DepartmentServiceInput = Readonly<{
9
+ serviceId: string;
10
+ serviceIdentifier: string;
11
+ name: string;
12
+ description?: string;
13
+ serviceCategory?: readonly string[];
14
+ serviceType: readonly string[];
15
+ specialty?: readonly string[];
16
+ locationReferences: readonly string[];
17
+ }>;
18
+ export type DepartmentServicesFlatGraphInput = Readonly<{
19
+ departmentId: string;
20
+ departmentIdentifier: string;
21
+ parentOrganizationReference: string;
22
+ name: string;
23
+ description?: string;
24
+ organizationType?: string;
25
+ services: readonly DepartmentServiceInput[];
26
+ }>;
27
+ /**
28
+ * Builds one department Organization and one independent HealthcareService
29
+ * row per offered service. Schedule actors can therefore target a blood,
30
+ * plasma or other service independently even when they share a department.
31
+ */
32
+ export declare function buildDepartmentServicesFlatGraph(input: DepartmentServicesFlatGraphInput): readonly ClaimsFirstGraphResource[];
@@ -0,0 +1,102 @@
1
+ import { HealthcareServiceClaim } from '../claims/healthcare-service-claims.js';
2
+ import { OrganizationClaim } from '../claims/organization-claims.js';
3
+ const required = (value, error) => {
4
+ const normalized = String(value || '').trim();
5
+ if (!normalized)
6
+ throw new TypeError(error);
7
+ return normalized;
8
+ };
9
+ const resourceId = (value) => {
10
+ const normalized = required(value, 'department_service_id_required');
11
+ if (!/^[A-Za-z0-9.-]{1,64}$/.test(normalized))
12
+ throw new TypeError('department_service_id_invalid');
13
+ return normalized;
14
+ };
15
+ const reference = (value, prefix, error) => {
16
+ const normalized = required(value, error);
17
+ if (!normalized.startsWith(`${prefix}/`) || normalized.length === prefix.length + 1)
18
+ throw new TypeError(error);
19
+ return normalized;
20
+ };
21
+ const token = (value, error) => {
22
+ const normalized = required(value, error);
23
+ const separator = normalized.lastIndexOf('|');
24
+ if (separator < 1 || separator === normalized.length - 1)
25
+ throw new TypeError(error);
26
+ return normalized;
27
+ };
28
+ const tokenList = (values, error, requiredList = false) => {
29
+ const normalized = Object.freeze((values ?? []).map(value => token(value, error)));
30
+ if (requiredList && !normalized.length)
31
+ throw new TypeError(error);
32
+ return normalized.length ? normalized : undefined;
33
+ };
34
+ const referenceList = (values, prefix, error) => {
35
+ const normalized = Object.freeze(values.map(value => reference(value, prefix, error)));
36
+ if (!normalized.length)
37
+ throw new TypeError(error);
38
+ return normalized;
39
+ };
40
+ const compactClaims = (claims) => Object.freeze(Object.fromEntries(Object.entries(claims).filter((entry) => entry[1] !== undefined)));
41
+ /**
42
+ * Builds one department Organization and one independent HealthcareService
43
+ * row per offered service. Schedule actors can therefore target a blood,
44
+ * plasma or other service independently even when they share a department.
45
+ */
46
+ export function buildDepartmentServicesFlatGraph(input) {
47
+ const departmentId = resourceId(input.departmentId);
48
+ const departmentIdentifier = token(input.departmentIdentifier, 'department_service_identifier_invalid');
49
+ const parentOrganizationReference = reference(input.parentOrganizationReference, 'Organization', 'department_service_parent_reference_invalid');
50
+ const name = required(input.name, 'department_service_name_required');
51
+ const description = input.description?.trim() || undefined;
52
+ const organizationType = input.organizationType
53
+ ? token(input.organizationType, 'department_service_organization_type_invalid')
54
+ : undefined;
55
+ const departmentReference = `Organization/${departmentId}`;
56
+ if (!input.services.length)
57
+ throw new TypeError('department_services_required');
58
+ const serviceIds = new Set();
59
+ const services = input.services.map(service => {
60
+ const serviceId = resourceId(service.serviceId);
61
+ if (serviceIds.has(serviceId))
62
+ throw new TypeError('department_service_id_duplicate');
63
+ serviceIds.add(serviceId);
64
+ const serviceIdentifier = token(service.serviceIdentifier, 'department_service_identifier_invalid');
65
+ const serviceName = required(service.name, 'department_service_name_required');
66
+ const serviceDescription = service.description?.trim() || undefined;
67
+ const serviceCategory = tokenList(service.serviceCategory, 'department_service_category_invalid');
68
+ const serviceType = tokenList(service.serviceType, 'department_service_type_invalid', true);
69
+ const specialty = tokenList(service.specialty, 'department_service_specialty_invalid');
70
+ const locationReferences = referenceList(service.locationReferences, 'Location', 'department_service_location_reference_invalid');
71
+ return Object.freeze({
72
+ resourceType: 'HealthcareService',
73
+ id: serviceId,
74
+ meta: Object.freeze({ claims: compactClaims({
75
+ [HealthcareServiceClaim.Identifier]: serviceIdentifier,
76
+ [HealthcareServiceClaim.Active]: 'true',
77
+ [HealthcareServiceClaim.Organization]: departmentReference,
78
+ [HealthcareServiceClaim.ServiceCategory]: serviceCategory,
79
+ [HealthcareServiceClaim.ServiceType]: serviceType,
80
+ [HealthcareServiceClaim.Specialty]: specialty,
81
+ [HealthcareServiceClaim.Location]: locationReferences,
82
+ [HealthcareServiceClaim.Name]: serviceName,
83
+ [HealthcareServiceClaim.Comment]: serviceDescription,
84
+ }) }),
85
+ });
86
+ });
87
+ return Object.freeze([
88
+ Object.freeze({
89
+ resourceType: 'Organization',
90
+ id: departmentId,
91
+ meta: Object.freeze({ claims: compactClaims({
92
+ [OrganizationClaim.Identifier]: departmentIdentifier,
93
+ [OrganizationClaim.Active]: 'true',
94
+ [OrganizationClaim.Type]: organizationType,
95
+ [OrganizationClaim.Name]: name,
96
+ [OrganizationClaim.Description]: description,
97
+ [OrganizationClaim.PartOf]: parentOrganizationReference,
98
+ }) }),
99
+ }),
100
+ ...services,
101
+ ]);
102
+ }
@@ -0,0 +1,51 @@
1
+ type FlatResourceType = 'Procedure' | 'DeviceMetric' | 'DocumentReference' | 'RegulatedAuthorization';
2
+ export type DeviceComplianceFlatResource = Readonly<{
3
+ resourceType: FlatResourceType;
4
+ id: string;
5
+ meta: Readonly<{
6
+ claims: Readonly<Record<string, string>>;
7
+ }>;
8
+ }>;
9
+ /**
10
+ * Builds the technical calibration event separately from the current metric state and its evidence.
11
+ * Performer references are descriptive; product authorization decides which credentialed actor may submit.
12
+ */
13
+ export declare function buildDeviceCalibrationFlatGraph(input: Readonly<{
14
+ procedureId: string;
15
+ procedureIdentifier: string;
16
+ procedureCode: string;
17
+ deviceReference: string;
18
+ metricId: string;
19
+ metricIdentifier: string;
20
+ metricType: string;
21
+ metricUnit?: string;
22
+ calibrationType: 'unspecified' | 'offset' | 'gain' | 'two-point';
23
+ calibrationState: 'not-calibrated' | 'calibration-required' | 'calibrated' | 'unspecified';
24
+ calibrationTime: string;
25
+ performerReferences: readonly string[];
26
+ locationReference?: string;
27
+ evidence?: Readonly<{
28
+ documentId: string;
29
+ documentIdentifier: string;
30
+ contentHash: string;
31
+ contentType: string;
32
+ }>;
33
+ }>): readonly DeviceComplianceFlatResource[];
34
+ /** Builds a public-authority operating authorization without conflating it with calibration. */
35
+ export declare function buildDeviceOperatingAuthorizationFlatResource(input: Readonly<{
36
+ authorizationId: string;
37
+ identifier: string;
38
+ subjectReference: string;
39
+ holderReference: string;
40
+ regulatorReference: string;
41
+ type: string;
42
+ status: string;
43
+ statusDate: string;
44
+ validityPeriod: Readonly<{
45
+ start: string;
46
+ end: string;
47
+ }>;
48
+ region: string;
49
+ basis: string;
50
+ }>): DeviceComplianceFlatResource;
51
+ export {};
@@ -0,0 +1,115 @@
1
+ import { DeviceMetricClaim } from '../claims/device-metric-claims.js';
2
+ import { DocumentReferenceClaim } from '../claims/document-reference-claims.js';
3
+ import { ProcedureClaim } from '../claims/procedure-claims.js';
4
+ import { RegulatedAuthorizationClaim } from '../claims/regulated-authorization-claims.js';
5
+ const required = (value, error) => {
6
+ const normalized = String(value || '').trim();
7
+ if (!normalized)
8
+ throw new TypeError(error);
9
+ return normalized;
10
+ };
11
+ const id = (value) => {
12
+ const normalized = required(value, 'device_compliance_id_required');
13
+ if (!/^[A-Za-z0-9.-]{1,64}$/.test(normalized))
14
+ throw new TypeError('device_compliance_id_invalid');
15
+ return normalized;
16
+ };
17
+ const reference = (value, resourceType, error) => {
18
+ const normalized = required(value, error);
19
+ if (!new RegExp(`^${resourceType}/[A-Za-z0-9.-]+$`).test(normalized))
20
+ throw new TypeError(error);
21
+ return normalized;
22
+ };
23
+ const actorReference = (value) => {
24
+ const normalized = required(value, 'calibration_performer_reference_invalid');
25
+ if (!/^(?:Practitioner|PractitionerRole|Organization|HealthcareService|Device)\/[A-Za-z0-9.-]+$/.test(normalized)) {
26
+ throw new TypeError('calibration_performer_reference_invalid');
27
+ }
28
+ return normalized;
29
+ };
30
+ const token = (value, error) => {
31
+ const normalized = required(value, error);
32
+ const separator = normalized.lastIndexOf('|');
33
+ if (separator < 1 || separator === normalized.length - 1)
34
+ throw new TypeError(error);
35
+ return normalized;
36
+ };
37
+ const instant = (value, error) => {
38
+ const normalized = required(value, error);
39
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(normalized))
40
+ throw new TypeError(error);
41
+ return normalized;
42
+ };
43
+ const resource = (resourceType, resourceId, claims) => Object.freeze({
44
+ resourceType,
45
+ id: id(resourceId),
46
+ meta: Object.freeze({ claims: Object.freeze(claims) }),
47
+ });
48
+ /**
49
+ * Builds the technical calibration event separately from the current metric state and its evidence.
50
+ * Performer references are descriptive; product authorization decides which credentialed actor may submit.
51
+ */
52
+ export function buildDeviceCalibrationFlatGraph(input) {
53
+ const deviceReference = reference(input.deviceReference, 'Device', 'device_reference_invalid');
54
+ const performers = [...new Set(input.performerReferences.map(actorReference))];
55
+ if (!performers.length)
56
+ throw new TypeError('calibration_performer_required');
57
+ const calibrationTime = instant(input.calibrationTime, 'calibration_time_invalid');
58
+ const reportReference = input.evidence ? `DocumentReference/${id(input.evidence.documentId)}` : undefined;
59
+ const procedure = resource('Procedure', input.procedureId, {
60
+ [ProcedureClaim.Identifier]: required(input.procedureIdentifier, 'procedure_identifier_required'),
61
+ [ProcedureClaim.Status]: 'completed',
62
+ [ProcedureClaim.Code]: token(input.procedureCode, 'procedure_code_invalid'),
63
+ [ProcedureClaim.Subject]: deviceReference,
64
+ [ProcedureClaim.FocalDevice]: deviceReference,
65
+ [ProcedureClaim.Date]: calibrationTime,
66
+ [ProcedureClaim.Performer]: performers.join(','),
67
+ ...(input.locationReference ? { [ProcedureClaim.Location]: reference(input.locationReference, 'Location', 'calibration_location_reference_invalid') } : {}),
68
+ ...(reportReference ? { [ProcedureClaim.Report]: reportReference } : {}),
69
+ });
70
+ const metric = resource('DeviceMetric', input.metricId, {
71
+ [DeviceMetricClaim.Identifier]: required(input.metricIdentifier, 'metric_identifier_required'),
72
+ [DeviceMetricClaim.Type]: token(input.metricType, 'metric_type_invalid'),
73
+ ...(input.metricUnit ? { [DeviceMetricClaim.Unit]: token(input.metricUnit, 'metric_unit_invalid') } : {}),
74
+ [DeviceMetricClaim.Device]: deviceReference,
75
+ [DeviceMetricClaim.Category]: 'measurement',
76
+ [DeviceMetricClaim.CalibrationType]: input.calibrationType,
77
+ [DeviceMetricClaim.CalibrationState]: input.calibrationState,
78
+ [DeviceMetricClaim.CalibrationTime]: calibrationTime,
79
+ });
80
+ const evidence = input.evidence
81
+ ? [resource('DocumentReference', input.evidence.documentId, {
82
+ [DocumentReferenceClaim.Identifier]: required(input.evidence.documentIdentifier, 'calibration_evidence_identifier_required'),
83
+ [DocumentReferenceClaim.Subject]: deviceReference,
84
+ [DocumentReferenceClaim.Context]: `Procedure/${procedure.id}`,
85
+ [DocumentReferenceClaim.ContentHash]: required(input.evidence.contentHash, 'calibration_evidence_hash_required'),
86
+ [DocumentReferenceClaim.ContentType]: required(input.evidence.contentType, 'calibration_evidence_content_type_required'),
87
+ [DocumentReferenceClaim.Date]: calibrationTime,
88
+ })]
89
+ : [];
90
+ return Object.freeze([procedure, metric, ...evidence]);
91
+ }
92
+ /** Builds a public-authority operating authorization without conflating it with calibration. */
93
+ export function buildDeviceOperatingAuthorizationFlatResource(input) {
94
+ const start = required(input.validityPeriod.start, 'authorization_validity_invalid');
95
+ const end = required(input.validityPeriod.end, 'authorization_validity_invalid');
96
+ if (Date.parse(end) < Date.parse(start))
97
+ throw new TypeError('authorization_validity_invalid');
98
+ const subjectReference = required(input.subjectReference, 'authorization_subject_reference_invalid');
99
+ if (!/^(?:DeviceDefinition|Organization|Location|ActivityDefinition|PlanDefinition)\/[A-Za-z0-9.-]+$/.test(subjectReference)) {
100
+ throw new TypeError('authorization_subject_reference_invalid');
101
+ }
102
+ return resource('RegulatedAuthorization', input.authorizationId, {
103
+ [RegulatedAuthorizationClaim.Identifier]: required(input.identifier, 'authorization_identifier_required'),
104
+ [RegulatedAuthorizationClaim.Subject]: subjectReference,
105
+ [RegulatedAuthorizationClaim.Type]: token(input.type, 'authorization_type_invalid'),
106
+ [RegulatedAuthorizationClaim.Region]: token(input.region, 'authorization_region_invalid'),
107
+ [RegulatedAuthorizationClaim.Status]: required(input.status, 'authorization_status_required'),
108
+ [RegulatedAuthorizationClaim.StatusDate]: instant(input.statusDate, 'authorization_status_date_invalid'),
109
+ [RegulatedAuthorizationClaim.ValidityStart]: start,
110
+ [RegulatedAuthorizationClaim.ValidityEnd]: end,
111
+ [RegulatedAuthorizationClaim.Basis]: token(input.basis, 'authorization_basis_invalid'),
112
+ [RegulatedAuthorizationClaim.Holder]: reference(input.holderReference, 'Organization', 'authorization_holder_reference_invalid'),
113
+ [RegulatedAuthorizationClaim.Regulator]: reference(input.regulatorReference, 'Organization', 'authorization_regulator_reference_invalid'),
114
+ });
115
+ }
@@ -1,3 +1,6 @@
1
1
  export * from './diagnostic-report-graph.js';
2
+ export * from './department-service-graph.js';
3
+ export * from './service-schedule-graph.js';
4
+ export * from './device-compliance-graph.js';
2
5
  export * from './immunization-graph.js';
3
6
  export * from './immunization-reminders.js';