fhir-data-utils-ts 0.6.9 → 0.6.11

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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Add the shared clinical-artifact editor boundary for native FHIR R4 imports:
6
+ hydrate only schema-owned fields, materialize supported IPS resources from
7
+ editor plus host-owned claims, and prove FHIR-to-form-to-FHIR semantic
8
+ round trips for every reusable IPS authoring schema.
9
+ - Give every reusable IPS authoring schema canonical initial claims so portal
10
+ components do not invent resource status defaults.
11
+
12
+ - Recover the useful locale-aware intent of the historical UHC
13
+ internationalization helpers as strict FHIR temporal presentation tools.
14
+ Date-only values retain their calendar day, instants render with the user's
15
+ locale and IANA time zone, editor date/time fields compose a canonical FHIR
16
+ instant with optional seconds defaulted to `00`, and inclusive ranges treat
17
+ an omitted upper bound as the supplied current instant.
18
+
5
19
  - Preserve locally entered `Observation.code.text` when a terminology code has
6
20
  not yet been confirmed, so profile-aware authoring remains visible and can be
7
21
  coded later without inventing a code.
package/README.md CHANGED
@@ -98,6 +98,12 @@ derived whole-second value; retain the original timezone-bearing instant and
98
98
  an independent `Encounter.id`. See
99
99
  [`docs/ENCOUNTER_IDENTIFIERS_AND_TIME.md`](docs/ENCOUNTER_IDENTIFIERS_AND_TIME.md).
100
100
 
101
+ Use the `/fhir-temporal-presentation` entry point at UI boundaries. It formats
102
+ FHIR date, dateTime and instant values with the user's locale and IANA time
103
+ zone, composes separate `YYYY-MM-DD` and `HH:mm[:ss]` editor fields, and
104
+ evaluates inclusive date-time ranges. ISO values remain the wire/storage form;
105
+ they are not user-facing labels.
106
+
101
107
  No consumer should copy claim paths or rebuild these structures with literals.
102
108
  Use `fhir-data-utils-ts/claims`, `/converters`, `/fhir-bundle`,
103
109
  `/fhir-search-parameters`, `/clinical-terminology`, `/group-r5` or
@@ -0,0 +1,17 @@
1
+ import { type FhirIpsClinicalArtifactSchema, type FhirIpsClinicalArtifactType } from './data/ips-clinical-artifact-schemas.data.js';
2
+ export type ClinicalArtifactFhirR4Resource = Readonly<Record<string, unknown> & {
3
+ resourceType: FhirIpsClinicalArtifactType;
4
+ }>;
5
+ /** Host-owned subject claims added only at the trusted materialization boundary. */
6
+ export declare function clinicalArtifactSubjectClaims(resourceType: FhirIpsClinicalArtifactType, subjectReference: string): Readonly<Record<string, string>>;
7
+ /**
8
+ * Hydrates one schema-driven editor from a native FHIR R4 resource.
9
+ *
10
+ * Only claims explicitly owned by the selected authoring schema are returned;
11
+ * subject, provenance and authorization claims remain host-owned.
12
+ */
13
+ export declare function clinicalArtifactEditorClaimsFromFhirR4(resource: ClinicalArtifactFhirR4Resource, schema: FhirIpsClinicalArtifactSchema): Readonly<Record<string, string>>;
14
+ /** Materializes a native FHIR R4 resource at the explicit editor/import boundary. */
15
+ export declare function clinicalArtifactFhirR4FromEditorClaims(resourceType: FhirIpsClinicalArtifactType, editorClaims: Readonly<Record<string, unknown>>, hostClaims?: Readonly<Record<string, unknown>>): ClinicalArtifactFhirR4Resource;
16
+ /** Converts a supported native FHIR R4 resource to its canonical flat claims. */
17
+ export declare function clinicalArtifactFhirR4ToFlat(resource: ClinicalArtifactFhirR4Resource): Readonly<Record<string, unknown>>;
@@ -0,0 +1,75 @@
1
+ // Copyright 2026 Connecting Solution & Applications Ltd under the Apache License, Version 2.0.
2
+ import { allergyIntoleranceFhirR4ToFlat, allergyIntoleranceFlatToFhirR4, conditionFhirR4ToFlat, conditionFlatToFhirR4, immunizationFhirR4ToFlat, immunizationFlatToFhirR4, medicationStatementFhirR4ToFlat, medicationStatementFlatToFhirR4, observationFromFlatToFhirR4, observationToFlatFhirR4, procedureFhirR4ToFlat, procedureFlatToFhirR4, } from './converters/index.js';
3
+ import { FhirIpsClinicalArtifactTypes, } from './data/ips-clinical-artifact-schemas.data.js';
4
+ import { AllergyIntoleranceClaim } from './claims/allergy-intolerance-claims.js';
5
+ import { ConditionClaim } from './claims/condition-claims.js';
6
+ import { ImmunizationClaim } from './claims/immunization-claims.js';
7
+ import { MedicationStatementClaim } from './claims/medication-statement-claims.js';
8
+ import { ProcedureClaim } from './claims/procedure-claims.js';
9
+ import { ObservationClaim } from './observation-claims.js';
10
+ /** Host-owned subject claims added only at the trusted materialization boundary. */
11
+ export function clinicalArtifactSubjectClaims(resourceType, subjectReference) {
12
+ const claim = {
13
+ [FhirIpsClinicalArtifactTypes.AllergyIntolerance]: AllergyIntoleranceClaim.Subject,
14
+ [FhirIpsClinicalArtifactTypes.MedicationStatement]: MedicationStatementClaim.Subject,
15
+ [FhirIpsClinicalArtifactTypes.Immunization]: ImmunizationClaim.Subject,
16
+ [FhirIpsClinicalArtifactTypes.Condition]: ConditionClaim.Subject,
17
+ [FhirIpsClinicalArtifactTypes.Procedure]: ProcedureClaim.Subject,
18
+ [FhirIpsClinicalArtifactTypes.Observation]: ObservationClaim.Subject,
19
+ }[resourceType];
20
+ return Object.freeze({ [claim]: subjectReference });
21
+ }
22
+ /**
23
+ * Hydrates one schema-driven editor from a native FHIR R4 resource.
24
+ *
25
+ * Only claims explicitly owned by the selected authoring schema are returned;
26
+ * subject, provenance and authorization claims remain host-owned.
27
+ */
28
+ export function clinicalArtifactEditorClaimsFromFhirR4(resource, schema) {
29
+ if (resource.resourceType !== schema.resourceType) {
30
+ throw new TypeError('clinical_artifact_schema_resource_type_mismatch');
31
+ }
32
+ const acceptedClaims = new Set([...schema.requiredClaims, ...schema.optionalClaims]);
33
+ const flatClaims = clinicalArtifactFhirR4ToFlat(resource);
34
+ const definedFlatClaims = Object.fromEntries(Object.entries(flatClaims).filter(([, value]) => value !== undefined && value !== null && value !== ''));
35
+ return Object.freeze(Object.fromEntries(Object.entries({ ...schema.initialClaims, ...definedFlatClaims })
36
+ .filter(([claim, value]) => acceptedClaims.has(claim) && value !== undefined && value !== null && value !== '')
37
+ .map(([claim, value]) => [claim, String(value)])));
38
+ }
39
+ /** Materializes a native FHIR R4 resource at the explicit editor/import boundary. */
40
+ export function clinicalArtifactFhirR4FromEditorClaims(resourceType, editorClaims, hostClaims = {}) {
41
+ const normalizedClaims = Object.fromEntries(Object.entries({ ...hostClaims, ...editorClaims })
42
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
43
+ .map(([claim, value]) => [claim, String(value)]));
44
+ switch (resourceType) {
45
+ case FhirIpsClinicalArtifactTypes.AllergyIntolerance:
46
+ return allergyIntoleranceFlatToFhirR4(normalizedClaims);
47
+ case FhirIpsClinicalArtifactTypes.MedicationStatement:
48
+ return medicationStatementFlatToFhirR4(normalizedClaims);
49
+ case FhirIpsClinicalArtifactTypes.Immunization:
50
+ return immunizationFlatToFhirR4(normalizedClaims);
51
+ case FhirIpsClinicalArtifactTypes.Condition:
52
+ return conditionFlatToFhirR4(normalizedClaims);
53
+ case FhirIpsClinicalArtifactTypes.Procedure:
54
+ return procedureFlatToFhirR4(normalizedClaims);
55
+ case FhirIpsClinicalArtifactTypes.Observation:
56
+ return observationFromFlatToFhirR4(normalizedClaims);
57
+ }
58
+ }
59
+ /** Converts a supported native FHIR R4 resource to its canonical flat claims. */
60
+ export function clinicalArtifactFhirR4ToFlat(resource) {
61
+ switch (resource.resourceType) {
62
+ case FhirIpsClinicalArtifactTypes.AllergyIntolerance:
63
+ return allergyIntoleranceFhirR4ToFlat(resource);
64
+ case FhirIpsClinicalArtifactTypes.MedicationStatement:
65
+ return medicationStatementFhirR4ToFlat(resource);
66
+ case FhirIpsClinicalArtifactTypes.Immunization:
67
+ return immunizationFhirR4ToFlat(resource);
68
+ case FhirIpsClinicalArtifactTypes.Condition:
69
+ return conditionFhirR4ToFlat(resource);
70
+ case FhirIpsClinicalArtifactTypes.Procedure:
71
+ return procedureFhirR4ToFlat(resource);
72
+ case FhirIpsClinicalArtifactTypes.Observation:
73
+ return observationToFlatFhirR4(resource);
74
+ }
75
+ }
@@ -67,7 +67,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
67
67
  display: string;
68
68
  }[];
69
69
  };
70
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
70
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
71
71
  id: string;
72
72
  status: string;
73
73
  subject: {
@@ -84,7 +84,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
84
84
  }[];
85
85
  };
86
86
  effectiveDateTime: string;
87
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
87
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
88
88
  id: string;
89
89
  subject: {
90
90
  reference: string;
@@ -103,7 +103,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
103
103
  }[];
104
104
  };
105
105
  occurrenceDateTime: string;
106
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
106
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
107
107
  id: string;
108
108
  subject: {
109
109
  reference: string;
@@ -119,7 +119,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
119
119
  }[];
120
120
  };
121
121
  effectiveDateTime: string;
122
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
122
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
123
123
  id: string;
124
124
  subject: {
125
125
  reference: string;
@@ -135,7 +135,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
135
135
  }[];
136
136
  };
137
137
  performedDateTime: string;
138
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
138
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
139
139
  id: string;
140
140
  subject: {
141
141
  reference: string;
@@ -145,7 +145,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
145
145
  device: {
146
146
  display: "Corrective lens use";
147
147
  };
148
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
148
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
149
149
  id: string;
150
150
  subject: {
151
151
  reference: string;
@@ -166,7 +166,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
166
166
  system: string;
167
167
  code: string;
168
168
  };
169
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
169
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
170
170
  id: string;
171
171
  status: string;
172
172
  subject: {
@@ -190,7 +190,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
190
190
  display: string;
191
191
  }[];
192
192
  };
193
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
193
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
194
194
  id: string;
195
195
  status: string;
196
196
  subject: {
@@ -206,7 +206,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
206
206
  display: string;
207
207
  }[];
208
208
  };
209
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
209
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
210
210
  id: string;
211
211
  subject: {
212
212
  reference: string;
@@ -221,7 +221,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
221
221
  display: string;
222
222
  }[];
223
223
  };
224
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
224
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
225
225
  id: string;
226
226
  status: string;
227
227
  subject: {
@@ -248,7 +248,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
248
248
  patient: {
249
249
  reference: string;
250
250
  };
251
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
251
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
252
252
  id: string;
253
253
  subject: {
254
254
  reference: string;
@@ -257,7 +257,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
257
257
  status: string;
258
258
  description: "Independent daily activities";
259
259
  summary: "Independent daily activities";
260
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
260
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
261
261
  id: string;
262
262
  subject: {
263
263
  reference: string;
@@ -274,7 +274,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
274
274
  display: string;
275
275
  }[];
276
276
  }[];
277
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
277
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
278
278
  id: string;
279
279
  subject: {
280
280
  reference: string;
@@ -291,7 +291,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
291
291
  display: string;
292
292
  }[];
293
293
  };
294
- resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "Procedure" | "DiagnosticReport" | "Observation" | "Immunization" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "Flag" | "MedicationStatement";
294
+ resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
295
295
  id: string;
296
296
  status: string;
297
297
  subject: {
@@ -14,6 +14,8 @@ export type FhirIpsClinicalArtifactSchema = Readonly<{
14
14
  optionalClaims: readonly string[];
15
15
  /** Source-text claims for which a user may explicitly confirm a terminology candidate. */
16
16
  terminologyTextClaims: readonly string[];
17
+ /** Canonical initial values used by every schema-driven editor. */
18
+ initialClaims: Readonly<Record<string, string>>;
17
19
  }>;
18
20
  export declare const FhirIpsObservationArtifactKinds: Readonly<{
19
21
  readonly VitalSigns: "vital-signs";
@@ -24,7 +26,6 @@ export declare const FhirIpsObservationArtifactKinds: Readonly<{
24
26
  export type FhirIpsObservationArtifactKind = typeof FhirIpsObservationArtifactKinds[keyof typeof FhirIpsObservationArtifactKinds];
25
27
  export type FhirIpsObservationArtifactSchema = FhirIpsClinicalArtifactSchema & Readonly<{
26
28
  kind: FhirIpsObservationArtifactKind;
27
- initialClaims: Readonly<Record<string, string>>;
28
29
  }>;
29
30
  /** Section-specific Observation authoring contracts; a resource type alone is not a sufficient selector. */
30
31
  export declare const FHIR_IPS_OBSERVATION_ARTIFACT_SCHEMAS: Readonly<Record<FhirIpsObservationArtifactKind, FhirIpsObservationArtifactSchema>>;
@@ -83,6 +83,7 @@ export const FHIR_IPS_CLINICAL_ARTIFACT_SCHEMAS = Object.freeze({
83
83
  requiredClaims: Object.freeze([AllergyIntoleranceClaim.CodeText, AllergyIntoleranceClaim.ClinicalStatus]),
84
84
  optionalClaims: Object.freeze([AllergyIntoleranceClaim.Code, AllergyIntoleranceClaim.CodeDisplay, AllergyIntoleranceClaim.Category, AllergyIntoleranceClaim.VerificationStatus, AllergyIntoleranceClaim.Criticality, AllergyIntoleranceClaim.OnsetDateTime, AllergyIntoleranceClaim.Recorder]),
85
85
  terminologyTextClaims: Object.freeze([AllergyIntoleranceClaim.CodeText]),
86
+ initialClaims: Object.freeze({ [AllergyIntoleranceClaim.ClinicalStatus]: 'active' }),
86
87
  }),
87
88
  [FhirIpsClinicalArtifactTypes.MedicationStatement]: Object.freeze({
88
89
  resourceType: FhirIpsClinicalArtifactTypes.MedicationStatement,
@@ -90,6 +91,7 @@ export const FHIR_IPS_CLINICAL_ARTIFACT_SCHEMAS = Object.freeze({
90
91
  requiredClaims: Object.freeze([MedicationStatementClaim.CodeText, MedicationStatementClaim.Status]),
91
92
  optionalClaims: Object.freeze([MedicationStatementClaim.Code, MedicationStatementClaim.CodeDisplay, MedicationStatementClaim.Category, MedicationStatementClaim.Effective, MedicationStatementClaim.DosageInstruction, MedicationStatementClaim.DosageRoute, MedicationStatementClaim.TimingFrequency, MedicationStatementClaim.TimingPeriod, MedicationStatementClaim.TimingPeriodUnit, MedicationStatementClaim.Note]),
92
93
  terminologyTextClaims: Object.freeze([MedicationStatementClaim.CodeText]),
94
+ initialClaims: Object.freeze({ [MedicationStatementClaim.Status]: 'active' }),
93
95
  }),
94
96
  [FhirIpsClinicalArtifactTypes.Immunization]: Object.freeze({
95
97
  resourceType: FhirIpsClinicalArtifactTypes.Immunization,
@@ -97,6 +99,7 @@ export const FHIR_IPS_CLINICAL_ARTIFACT_SCHEMAS = Object.freeze({
97
99
  requiredClaims: Object.freeze([ImmunizationClaim.VaccineCodeText, ImmunizationClaim.Status]),
98
100
  optionalClaims: Object.freeze([ImmunizationClaim.VaccineCode, ImmunizationClaim.VaccineCodeDisplay, ImmunizationClaim.Date, ImmunizationClaim.LotNumber, ImmunizationClaim.DoseSequence, ImmunizationClaim.Manufacturer, ImmunizationClaim.Performer, ImmunizationClaim.Location, ImmunizationClaim.Route, ImmunizationClaim.RouteDisplay, ImmunizationClaim.Site, ImmunizationClaim.SiteDisplay, ImmunizationClaim.Series, ImmunizationClaim.TargetDisease, ImmunizationClaim.TargetDiseaseText, ImmunizationClaim.TargetDiseaseDisplay, ImmunizationClaim.ReactionDate, ImmunizationClaim.Note]),
99
101
  terminologyTextClaims: Object.freeze([ImmunizationClaim.VaccineCodeText, ImmunizationClaim.TargetDiseaseText]),
102
+ initialClaims: Object.freeze({ [ImmunizationClaim.Status]: 'completed' }),
100
103
  }),
101
104
  [FhirIpsClinicalArtifactTypes.Condition]: Object.freeze({
102
105
  resourceType: FhirIpsClinicalArtifactTypes.Condition,
@@ -104,6 +107,7 @@ export const FHIR_IPS_CLINICAL_ARTIFACT_SCHEMAS = Object.freeze({
104
107
  requiredClaims: Object.freeze([ConditionClaim.CodeText, ConditionClaim.ClinicalStatus]),
105
108
  optionalClaims: Object.freeze([ConditionClaim.Code, ConditionClaim.CodeDisplay, ConditionClaim.Category, ConditionClaim.VerificationStatus, ConditionClaim.Severity, ConditionClaim.OnsetDateTime, ConditionClaim.RecordedDate, ConditionClaim.Asserter, ConditionClaim.Recorder]),
106
109
  terminologyTextClaims: Object.freeze([ConditionClaim.CodeText]),
110
+ initialClaims: Object.freeze({ [ConditionClaim.ClinicalStatus]: 'active' }),
107
111
  }),
108
112
  [FhirIpsClinicalArtifactTypes.Procedure]: Object.freeze({
109
113
  resourceType: FhirIpsClinicalArtifactTypes.Procedure,
@@ -111,6 +115,7 @@ export const FHIR_IPS_CLINICAL_ARTIFACT_SCHEMAS = Object.freeze({
111
115
  requiredClaims: Object.freeze([ProcedureClaim.CodeText, ProcedureClaim.Status]),
112
116
  optionalClaims: Object.freeze([ProcedureClaim.Code, ProcedureClaim.CodeDisplay, ProcedureClaim.Date, ProcedureClaim.Performer, ProcedureClaim.Location, ProcedureClaim.BodySite, ProcedureClaim.ReasonCode, ProcedureClaim.ReasonReference, ProcedureClaim.Note]),
113
117
  terminologyTextClaims: Object.freeze([ProcedureClaim.CodeText]),
118
+ initialClaims: Object.freeze({ [ProcedureClaim.Status]: 'completed' }),
114
119
  }),
115
120
  [FhirIpsClinicalArtifactTypes.Observation]: Object.freeze({
116
121
  ...FHIR_IPS_OBSERVATION_ARTIFACT_SCHEMAS[FhirIpsObservationArtifactKinds.VitalSigns],
@@ -0,0 +1,27 @@
1
+ export type FhirTemporalRange = Readonly<{
2
+ from?: string;
3
+ to?: string;
4
+ }>;
5
+ export type LocalDateTimeParts = Readonly<{
6
+ date: string;
7
+ time: string;
8
+ }>;
9
+ export type ComposeFhirDateTimeInput = LocalDateTimeParts & Readonly<{
10
+ timeZone: string;
11
+ }>;
12
+ export type FhirTemporalFormatOptions = Readonly<{
13
+ timeZone?: string;
14
+ includeTime?: boolean;
15
+ includeSeconds?: boolean;
16
+ }>;
17
+ /**
18
+ * Formats a FHIR date, dateTime or instant for a human in their locale.
19
+ * Wire values remain unchanged; ISO syntax is never used as presentation.
20
+ */
21
+ export declare function formatFhirTemporalForLocale(value: string | Date, locale: string, options?: FhirTemporalFormatOptions): string;
22
+ /** Builds a FHIR instant from separate browser-friendly local date and time fields. */
23
+ export declare function composeFhirDateTimeFromLocalParts(input: ComposeFhirDateTimeInput): string;
24
+ /** Splits a FHIR instant into local inputs without leaking ISO UTC presentation. */
25
+ export declare function splitFhirDateTimeForInput(value: string | Date, timeZone: string): LocalDateTimeParts;
26
+ /** Applies an inclusive range; an omitted upper bound means the supplied current instant. */
27
+ export declare function isFhirTemporalWithinRange(value: string | Date, range: FhirTemporalRange, now?: Date): boolean;
@@ -0,0 +1,111 @@
1
+ const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
2
+ const TIME_PATTERN = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
3
+ /**
4
+ * Formats a FHIR date, dateTime or instant for a human in their locale.
5
+ * Wire values remain unchanged; ISO syntax is never used as presentation.
6
+ */
7
+ export function formatFhirTemporalForLocale(value, locale, options = {}) {
8
+ const dateOnly = typeof value === 'string' ? DATE_PATTERN.exec(value) : null;
9
+ const includeTime = options.includeTime ?? !dateOnly;
10
+ const date = dateOnly
11
+ ? new Date(Date.UTC(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3])))
12
+ : value instanceof Date ? value : new Date(value);
13
+ if (Number.isNaN(date.getTime()))
14
+ throw new RangeError('FHIR temporal value is invalid');
15
+ return new Intl.DateTimeFormat(locale, {
16
+ year: 'numeric',
17
+ month: dateOnly ? '2-digit' : 'short',
18
+ day: dateOnly ? '2-digit' : 'numeric',
19
+ hour: includeTime ? '2-digit' : undefined,
20
+ minute: includeTime ? '2-digit' : undefined,
21
+ second: includeTime && options.includeSeconds ? '2-digit' : undefined,
22
+ hour12: includeTime ? false : undefined,
23
+ timeZone: dateOnly ? 'UTC' : options.timeZone,
24
+ }).format(date);
25
+ }
26
+ /** Builds a FHIR instant from separate browser-friendly local date and time fields. */
27
+ export function composeFhirDateTimeFromLocalParts(input) {
28
+ const dateMatch = DATE_PATTERN.exec(input.date);
29
+ const timeMatch = TIME_PATTERN.exec(input.time);
30
+ if (!dateMatch || !timeMatch)
31
+ throw new RangeError('Use YYYY-MM-DD and HH:mm or HH:mm:ss');
32
+ const expected = {
33
+ year: Number(dateMatch[1]),
34
+ month: Number(dateMatch[2]),
35
+ day: Number(dateMatch[3]),
36
+ hour: Number(timeMatch[1]),
37
+ minute: Number(timeMatch[2]),
38
+ second: Number(timeMatch[3] ?? '00'),
39
+ };
40
+ assertValidCalendarParts(expected);
41
+ assertTimeZone(input.timeZone);
42
+ const expectedUtc = Date.UTC(expected.year, expected.month - 1, expected.day, expected.hour, expected.minute, expected.second);
43
+ let candidate = expectedUtc;
44
+ for (let attempt = 0; attempt < 3; attempt += 1) {
45
+ const actual = datePartsInTimeZone(new Date(candidate), input.timeZone);
46
+ const actualUtc = Date.UTC(actual.year, actual.month - 1, actual.day, actual.hour, actual.minute, actual.second);
47
+ candidate += expectedUtc - actualUtc;
48
+ }
49
+ const result = new Date(candidate);
50
+ const roundTrip = datePartsInTimeZone(result, input.timeZone);
51
+ if (Object.keys(expected).some((key) => expected[key] !== roundTrip[key])) {
52
+ throw new RangeError('Local date and time do not exist in the selected time zone');
53
+ }
54
+ return result.toISOString();
55
+ }
56
+ /** Splits a FHIR instant into local inputs without leaking ISO UTC presentation. */
57
+ export function splitFhirDateTimeForInput(value, timeZone) {
58
+ assertTimeZone(timeZone);
59
+ const date = value instanceof Date ? value : new Date(value);
60
+ if (Number.isNaN(date.getTime()))
61
+ throw new RangeError('FHIR temporal value is invalid');
62
+ const parts = datePartsInTimeZone(date, timeZone);
63
+ return {
64
+ date: `${parts.year}-${pad(parts.month)}-${pad(parts.day)}`,
65
+ time: `${pad(parts.hour)}:${pad(parts.minute)}:${pad(parts.second)}`,
66
+ };
67
+ }
68
+ /** Applies an inclusive range; an omitted upper bound means the supplied current instant. */
69
+ export function isFhirTemporalWithinRange(value, range, now = new Date()) {
70
+ const instant = value instanceof Date ? value : new Date(value);
71
+ const from = range.from ? new Date(range.from) : undefined;
72
+ const to = range.to ? new Date(range.to) : now;
73
+ if ([instant, from, to].some((date) => date && Number.isNaN(date.getTime()))) {
74
+ throw new RangeError('FHIR temporal range is invalid');
75
+ }
76
+ return (!from || instant >= from) && instant <= to;
77
+ }
78
+ function datePartsInTimeZone(date, timeZone) {
79
+ const values = new Map(new Intl.DateTimeFormat('en-CA', {
80
+ timeZone,
81
+ year: 'numeric', month: '2-digit', day: '2-digit',
82
+ hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
83
+ }).formatToParts(date).map(({ type, value }) => [type, value]));
84
+ return {
85
+ year: Number(values.get('year')),
86
+ month: Number(values.get('month')),
87
+ day: Number(values.get('day')),
88
+ hour: Number(values.get('hour')),
89
+ minute: Number(values.get('minute')),
90
+ second: Number(values.get('second')),
91
+ };
92
+ }
93
+ function assertValidCalendarParts(parts) {
94
+ if (parts.hour > 23 || parts.minute > 59 || parts.second > 59)
95
+ throw new RangeError('Local time is invalid');
96
+ const date = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
97
+ if (date.getUTCFullYear() !== parts.year || date.getUTCMonth() + 1 !== parts.month || date.getUTCDate() !== parts.day) {
98
+ throw new RangeError('Local date is invalid');
99
+ }
100
+ }
101
+ function assertTimeZone(timeZone) {
102
+ try {
103
+ new Intl.DateTimeFormat('en', { timeZone }).format(0);
104
+ }
105
+ catch {
106
+ throw new RangeError('IANA time zone is invalid');
107
+ }
108
+ }
109
+ function pad(value) {
110
+ return String(value).padStart(2, '0');
111
+ }
package/dist/index.d.ts CHANGED
@@ -27,3 +27,5 @@ export * from './healthcare-service-specialty-map.js';
27
27
  export * from './data/index.js';
28
28
  export * from './fhir-instant.js';
29
29
  export * from './health-credential-qr.js';
30
+ export * from './fhir-temporal-presentation.js';
31
+ export * from './clinical-artifact-editor.js';
package/dist/index.js CHANGED
@@ -27,3 +27,5 @@ export * from './healthcare-service-specialty-map.js';
27
27
  export * from './data/index.js';
28
28
  export * from './fhir-instant.js';
29
29
  export * from './health-credential-qr.js';
30
+ export * from './fhir-temporal-presentation.js';
31
+ export * from './clinical-artifact-editor.js';
@@ -154,6 +154,19 @@ Converters and builders import catalog symbols. Tests may spell native FHIR
154
154
  wire fields, but do not duplicate flat-claim paths except when proving that a
155
155
  malformed or invented path is rejected.
156
156
 
157
+ ## Schema-driven editor boundary
158
+
159
+ `clinicalArtifactFhirR4ToFlat(...)` and
160
+ `clinicalArtifactEditorClaimsFromFhirR4(...)` are import/read adapters. They
161
+ turn a native FHIR R4 clinical resource into canonical flat claims and never
162
+ own a portal form, authorization or transport decision.
163
+
164
+ The cross-channel JSON Schema and the `BundleEditor`/`BundleReader` round trip
165
+ belong to `sos-data-utils-ts`. A shared React component consumes both layers:
166
+ FHIR conversion for native imports, then the SOS JSON Schema to select and
167
+ validate editable values and create the claims-first changes Bundle. Portals
168
+ must not add resource-specific claim mappers between those shared layers.
169
+
157
170
  ## Subscription notification boundary
158
171
 
159
172
  FHIR R5 `SubscriptionTopic` and `Subscription` are native configuration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhir-data-utils-ts",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "FHIR-version-neutral flat-claim and SearchParameter data utilities",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -111,6 +111,10 @@
111
111
  "types": "./dist/health-credential-qr.d.ts",
112
112
  "default": "./dist/health-credential-qr.js"
113
113
  },
114
+ "./fhir-temporal-presentation": {
115
+ "types": "./dist/fhir-temporal-presentation.d.ts",
116
+ "default": "./dist/fhir-temporal-presentation.js"
117
+ },
114
118
  "./research-study-r5": {
115
119
  "types": "./dist/research-study-r5.d.ts",
116
120
  "default": "./dist/research-study-r5.js"