fhir-data-utils-ts 0.3.1 → 0.3.3

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.
@@ -19,6 +19,12 @@ redeclare governed claim paths, code systems, UUIDs or example values.
19
19
  - Each proposal `field` names the coded target claim. Confirmation writes
20
20
  `system|code` to that target and the terminology display to `-display`; it
21
21
  must preserve the imported `-text` value.
22
+ - Review state comes only from `meta.codingProposals[].status`. An accepted
23
+ professional decision sets that proposal's `userSelected=true`; never use
24
+ `userSelected` as a draft or promotion flag.
25
+ - Group review UI with the package-owned clinical families: allergies and
26
+ intolerances, procedures, diagnostics and immunizations. Do not duplicate
27
+ resource-type lists in Vet, UHC or DataConv consumers.
22
28
  - A localized generated label may use a language-qualified `-text_<BCP47>`
23
29
  companion. It must not overwrite the imported local text before explicit
24
30
  review.
@@ -28,6 +34,12 @@ redeclare governed claim paths, code systems, UUIDs or example values.
28
34
  `entry[].data[]`.
29
35
  - Multiple imported rows may resolve to one stable UUID ResearchSubject. Keep
30
36
  every proposal on the clinical resource generated from its source row.
37
+ - Complete each ResearchSubject independently when all of its own mandatory
38
+ proposals are resolved. Other subjects in the same import thread never block
39
+ it, and review always resumes from persisted resources without re-upload.
40
+ - Text supplied by an individual, a source table or an automated/device flow
41
+ may create proposals, but does not mark an inferred code professionally
42
+ reviewed. Preserve origin/provenance separately from the review decision.
31
43
 
32
44
  ## Workflow
33
45
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.3 - 2026-09-23
4
+
5
+ - Define product-neutral coding-review families and pending-proposal helpers.
6
+ - Keep review workflow in `meta.codingProposals[].status`; reserve
7
+ `userSelected=true` for a code explicitly selected by a reviewer.
8
+
9
+ ## 0.3.2 - 2026-09-23
10
+
11
+ - Preserve every coding identity from repeatable AllergyIntolerance and
12
+ MedicationStatement CodeableConcepts instead of retaining only `coding[0]`.
13
+ - Keep the MedicationStatement dose quantity's human `unit` separate from its
14
+ coded `system|code`, with read compatibility for records written by 0.3.1.
15
+ - Add executable regressions using the exact allergy and medication examples
16
+ from the HL7 IPS all-sections Bundle.
17
+
3
18
  ## 0.3.1 - 2026-09-21
4
19
 
5
20
  - Separate canonical claim catalogs from graph builders and move Immunization
package/README.md CHANGED
@@ -55,6 +55,14 @@ Use `fhir-data-utils-ts/claims`, `/converters`, `/fhir-bundle`,
55
55
  `/fhir-search-parameters`, `/clinical-terminology`, `/group-r5` or
56
56
  `/research-study-r5` as appropriate.
57
57
 
58
+ Repeatable `CodeableConcept.coding[]` identities are serialized in the owning
59
+ token claim as a comma-separated list of complete `system|code` tokens. The
60
+ first `*-display` remains source evidence for compatibility; authoritative
61
+ labels for every coding are resolved from each token at the terminology
62
+ boundary. FHIR `Quantity.unit` remains separate from its coded identity:
63
+ MedicationStatement dosage uses `dose-quantity-unit` for the human unit and
64
+ `dose-quantity` for `system|code`.
65
+
58
66
  ## Observation components
59
67
 
60
68
  Composite observations use a claims-first resource graph. The primary
@@ -109,3 +117,15 @@ claim strings, terminology identifiers, codes or UUIDs.
109
117
  coverage over converters, builders and core FHIR utilities. The package also
110
118
  tests every stable export subpath. Release validation additionally inspects the
111
119
  packed artifact and installs it in an isolated consumer before publication.
120
+ # Coding-review workflow
121
+
122
+ Imported wording remains in the resource's canonical `*-text` flat claim. Each
123
+ unconfirmed terminology suggestion lives beside `meta.claims` in the owning
124
+ contained resource's `meta.codingProposals[]`; it is never moved to
125
+ `ResearchSubject.meta`.
126
+
127
+ Review state comes only from `codingProposals[].status`. A Coding
128
+ `userSelected=true` flat claim records that a human selected the accepted code;
129
+ it is not a draft, publication or promotion flag. Consumers can use the shared
130
+ review families `allergies-intolerances`, `procedures`, `diagnostics` and
131
+ `immunizations` without product-specific literals.
@@ -13,6 +13,7 @@ export type FlatClaimCodingProposal = Readonly<{
13
13
  language: string;
14
14
  candidates: readonly FlatClaimCodingCandidate[];
15
15
  selectedCandidateId?: string;
16
+ userSelected?: boolean;
16
17
  }>;
17
18
  export type FlatClaimCodingReviewResource = Readonly<{
18
19
  resourceType: string;
@@ -43,6 +44,18 @@ export type ContainedCodingReviewItem = Readonly<{
43
44
  resource: FlatClaimCodingReviewResource;
44
45
  proposal: FlatClaimCodingProposal;
45
46
  }>;
47
+ export declare const CODING_REVIEW_CATEGORIES: Readonly<{
48
+ readonly AllergiesIntolerances: "allergies-intolerances";
49
+ readonly Procedures: "procedures";
50
+ readonly Diagnostics: "diagnostics";
51
+ readonly Immunizations: "immunizations";
52
+ readonly Other: "other";
53
+ }>;
54
+ export type CodingReviewCategory = typeof CODING_REVIEW_CATEGORIES[keyof typeof CODING_REVIEW_CATEGORIES];
55
+ /** Map a FHIR resource type to its stable, product-neutral coding-review family. */
56
+ export declare function codingReviewCategoryForResourceType(resourceType: string): CodingReviewCategory;
57
+ /** A proposal remains reviewable until a reviewer accepts, rejects or defers it. */
58
+ export declare function isCodingProposalPending(proposal: FlatClaimCodingProposal): boolean;
46
59
  /** Return the canonical local-text companion for a code-like flat claim. */
47
60
  export declare function deriveCodingLocalTextClaim(targetClaim: string): string;
48
61
  /** Return the official-display companion for a code-like flat claim. */
@@ -66,6 +79,8 @@ export declare function buildFlatClaimCodingReviewResource(input: Readonly<{
66
79
  }>): FlatClaimCodingReviewResource;
67
80
  /** Traverse every ResearchSubject entry and return resource-scoped proposals. */
68
81
  export declare function collectBatchCodingReviewItems(batch: FhirLikeCodingReviewBatch): readonly ContainedCodingReviewItem[];
82
+ /** Return only unresolved contained-resource proposals; resource userSelected is not workflow state. */
83
+ export declare function collectPendingBatchCodingReviewItems(batch: FhirLikeCodingReviewBatch): readonly ContainedCodingReviewItem[];
69
84
  /**
70
85
  * Confirm one terminology candidate on the resource that owns the proposal.
71
86
  * The coded claim and official display are added while the imported local
@@ -1,4 +1,27 @@
1
1
  import { buildFlatClaimResourceEntry } from './flat-claim-resource-graph.js';
2
+ export const CODING_REVIEW_CATEGORIES = Object.freeze({
3
+ AllergiesIntolerances: 'allergies-intolerances',
4
+ Procedures: 'procedures',
5
+ Diagnostics: 'diagnostics',
6
+ Immunizations: 'immunizations',
7
+ Other: 'other',
8
+ });
9
+ const CODING_REVIEW_RESOURCE_CATEGORIES = Object.freeze({
10
+ AllergyIntolerance: CODING_REVIEW_CATEGORIES.AllergiesIntolerances,
11
+ Procedure: CODING_REVIEW_CATEGORIES.Procedures,
12
+ Condition: CODING_REVIEW_CATEGORIES.Diagnostics,
13
+ DiagnosticReport: CODING_REVIEW_CATEGORIES.Diagnostics,
14
+ Observation: CODING_REVIEW_CATEGORIES.Diagnostics,
15
+ Immunization: CODING_REVIEW_CATEGORIES.Immunizations,
16
+ });
17
+ /** Map a FHIR resource type to its stable, product-neutral coding-review family. */
18
+ export function codingReviewCategoryForResourceType(resourceType) {
19
+ return CODING_REVIEW_RESOURCE_CATEGORIES[resourceType] ?? CODING_REVIEW_CATEGORIES.Other;
20
+ }
21
+ /** A proposal remains reviewable until a reviewer accepts, rejects or defers it. */
22
+ export function isCodingProposalPending(proposal) {
23
+ return proposal.status === 'proposed';
24
+ }
2
25
  /** Return the canonical local-text companion for a code-like flat claim. */
3
26
  export function deriveCodingLocalTextClaim(targetClaim) {
4
27
  return `${requiredCodingTarget(targetClaim)}-text`;
@@ -54,6 +77,10 @@ export function collectBatchCodingReviewItems(batch) {
54
77
  }
55
78
  return Object.freeze(items);
56
79
  }
80
+ /** Return only unresolved contained-resource proposals; resource userSelected is not workflow state. */
81
+ export function collectPendingBatchCodingReviewItems(batch) {
82
+ return Object.freeze(collectBatchCodingReviewItems(batch).filter(item => isCodingProposalPending(item.proposal)));
83
+ }
57
84
  /**
58
85
  * Confirm one terminology candidate on the resource that owns the proposal.
59
86
  * The coded claim and official display are added while the imported local
@@ -72,6 +99,7 @@ export function applyFlatClaimCodingSelection(resource, proposalId, candidateId)
72
99
  ...proposal,
73
100
  status: 'accepted',
74
101
  selectedCandidateId: candidate.id,
102
+ userSelected: true,
75
103
  });
76
104
  const updatedProposals = proposals.map((item, index) => (index === proposalIndex ? updatedProposal : item));
77
105
  return Object.freeze({
@@ -81,6 +109,7 @@ export function applyFlatClaimCodingSelection(resource, proposalId, candidateId)
81
109
  ...resource.meta.claims,
82
110
  [proposal.field]: `${candidate.system}|${candidate.code}`,
83
111
  [deriveCodingDisplayClaim(proposal.field)]: candidate.display,
112
+ [`${resource.resourceType}.userSelected`]: 'true',
84
113
  }),
85
114
  codingProposals: Object.freeze(updatedProposals),
86
115
  }),
@@ -116,6 +145,7 @@ const CONDITION_EXPECTED_ACCEPTED_CLAIMS = Object.freeze({
116
145
  ...CONDITION_EXAMPLE_RESOURCE.meta.claims,
117
146
  [CONDITION_EXAMPLE_INPUT.targetClaim]: `${CONDITION_EXAMPLE_CANDIDATE.system}|${CONDITION_EXAMPLE_CANDIDATE.code}`,
118
147
  [deriveCodingDisplayClaim(CONDITION_EXAMPLE_INPUT.targetClaim)]: CONDITION_EXAMPLE_CANDIDATE.display,
148
+ [`${CONDITION_EXAMPLE_RESOURCE.resourceType}.userSelected`]: 'true',
119
149
  });
120
150
  /** Package-owned reusable fixture; tests and consumer examples must import it. */
121
151
  export const CODING_REVIEW_EXAMPLES = Object.freeze({
@@ -1,7 +1,7 @@
1
1
  // Copyright 2026 Conéctate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
2
2
  // File: src/utils/convert-allergy-intolerance.ts
3
3
  import { AllergyIntoleranceClaim } from '../claims/allergy-intolerance-claims.js';
4
- import { codingFromValue, codingToValue, requireDidWeb, requireSubjectIdentifier } from './convert-shared.js';
4
+ import { codingFromValue, codingListFromCsv, codingListToCsv, codingToValue, requireDidWeb, requireSubjectIdentifier, } from './convert-shared.js';
5
5
  export function allergyIntoleranceFlatToFhirR4(claims) {
6
6
  const patient = claims[AllergyIntoleranceClaim.Subject] ?? claims[AllergyIntoleranceClaim.Patient];
7
7
  const recorder = claims[AllergyIntoleranceClaim.Recorder];
@@ -23,9 +23,9 @@ export function allergyIntoleranceFlatToFhirR4(claims) {
23
23
  patient: { reference: patient },
24
24
  code: claims[AllergyIntoleranceClaim.Code]
25
25
  ? {
26
- coding: codingFromValue(claims[AllergyIntoleranceClaim.Code])?.map((coding) => ({
26
+ coding: codingListFromCsv(claims[AllergyIntoleranceClaim.Code])?.map((coding, index) => ({
27
27
  ...coding,
28
- ...(claims[AllergyIntoleranceClaim.CodeDisplay]
28
+ ...(index === 0 && claims[AllergyIntoleranceClaim.CodeDisplay]
29
29
  ? { display: claims[AllergyIntoleranceClaim.CodeDisplay] }
30
30
  : {}),
31
31
  })),
@@ -64,7 +64,7 @@ export function allergyIntoleranceFhirR4ToFlat(resource) {
64
64
  [AllergyIntoleranceClaim.Identifier]: resource.identifier?.[0]?.value,
65
65
  [AllergyIntoleranceClaim.Subject]: subject,
66
66
  [AllergyIntoleranceClaim.Patient]: subject,
67
- [AllergyIntoleranceClaim.Code]: codingToValue(code?.coding?.[0]) || code?.text,
67
+ [AllergyIntoleranceClaim.Code]: codingListToCsv(code?.coding) || code?.text,
68
68
  [AllergyIntoleranceClaim.CodeText]: code?.text,
69
69
  [AllergyIntoleranceClaim.CodeDisplay]: code?.coding?.[0]?.display,
70
70
  [AllergyIntoleranceClaim.ClinicalStatus]: resource.clinicalStatus?.coding?.[0]?.code,
@@ -1,7 +1,18 @@
1
1
  // Copyright 2026 Conéctate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
2
2
  // File: src/utils/convert-medication-statement.ts
3
- import { MedicationStatementClaim } from '../claims/medication-statement-claims.js';
4
- import { codingFromValue, codingToValue, requireClaim } from './convert-shared.js';
3
+ import { MedicationStatementClaim, MedicationStatementClaimsFhirApiExtended, } from '../claims/medication-statement-claims.js';
4
+ import { codingFromValue, codingListFromCsv, codingListToCsv, codingToValue, requireClaim, } from './convert-shared.js';
5
+ function isCodingToken(value) {
6
+ return Boolean(value?.includes('|'));
7
+ }
8
+ function conceptCodings(claims) {
9
+ return codingListFromCsv(claims[MedicationStatementClaim.Code])?.map((coding, index) => ({
10
+ ...coding,
11
+ ...(index === 0 && claims[MedicationStatementClaim.CodeDisplay]
12
+ ? { display: claims[MedicationStatementClaim.CodeDisplay] }
13
+ : {}),
14
+ }));
15
+ }
5
16
  export function medicationStatementFlatToFhirR4(claims) {
6
17
  const subject = requireClaim(claims, MedicationStatementClaim.Subject);
7
18
  const status = requireClaim(claims, MedicationStatementClaim.Status);
@@ -24,10 +35,7 @@ export function medicationStatementFlatToFhirR4(claims) {
24
35
  id: containedMedicationId,
25
36
  identifier: medicationIdentifier ? [{ value: medicationIdentifier }] : undefined,
26
37
  code: claims[MedicationStatementClaim.Code] || medicationText ? {
27
- coding: codingFromValue(claims[MedicationStatementClaim.Code])?.map((coding) => ({
28
- ...coding,
29
- ...(claims[MedicationStatementClaim.CodeDisplay] ? { display: claims[MedicationStatementClaim.CodeDisplay] } : {}),
30
- })),
38
+ coding: conceptCodings(claims),
31
39
  ...(medicationText ? { text: medicationText } : {}),
32
40
  } : undefined,
33
41
  batch: medicationSerialNumber || medicationExpirationDate ? {
@@ -40,7 +48,10 @@ export function medicationStatementFlatToFhirR4(claims) {
40
48
  const dosagePeriod = claims[MedicationStatementClaim.TimingPeriod];
41
49
  const dosagePeriodUnit = claims[MedicationStatementClaim.TimingPeriodUnit];
42
50
  const doseValue = claims[MedicationStatementClaim.DoseQuantityValue];
43
- const doseUnit = claims[MedicationStatementClaim.DoseQuantityUnit];
51
+ const storedDoseUnit = claims[MedicationStatementClaim.DoseQuantityUnit];
52
+ const doseUnitToken = claims[MedicationStatementClaimsFhirApiExtended.DoseQuantity]
53
+ || (isCodingToken(storedDoseUnit) ? storedDoseUnit : undefined);
54
+ const doseUnitText = isCodingToken(storedDoseUnit) ? undefined : storedDoseUnit;
44
55
  const hasDosage = claims[MedicationStatementClaim.DosageInstruction] || dosageRoute || dosageFrequency || dosagePeriod || doseValue;
45
56
  return {
46
57
  resourceType: 'MedicationStatement',
@@ -54,10 +65,7 @@ export function medicationStatementFlatToFhirR4(claims) {
54
65
  } : undefined,
55
66
  medicationCodeableConcept: !medicationReference && !hasContainedMedication && claims[MedicationStatementClaim.Code]
56
67
  ? {
57
- coding: codingFromValue(claims[MedicationStatementClaim.Code])?.map((coding) => ({
58
- ...coding,
59
- ...(claims[MedicationStatementClaim.CodeDisplay] ? { display: claims[MedicationStatementClaim.CodeDisplay] } : {}),
60
- })),
68
+ coding: conceptCodings(claims),
61
69
  ...(medicationText ? { text: medicationText } : {}),
62
70
  }
63
71
  : (!medicationReference && !hasContainedMedication && medicationText ? { text: medicationText } : undefined),
@@ -76,8 +84,8 @@ export function medicationStatementFlatToFhirR4(claims) {
76
84
  } } : undefined,
77
85
  doseAndRate: doseValue ? [{ doseQuantity: {
78
86
  value: Number(doseValue),
79
- unit: doseUnit,
80
- ...(codingFromValue(doseUnit)?.[0] || {}),
87
+ ...(doseUnitText ? { unit: doseUnitText } : {}),
88
+ ...(codingFromValue(doseUnitToken)?.[0] || {}),
81
89
  } }] : undefined,
82
90
  }] : undefined,
83
91
  note: claims[MedicationStatementClaim.Note] ? [{ text: claims[MedicationStatementClaim.Note] }] : undefined,
@@ -94,7 +102,8 @@ export function medicationStatementFhirR4ToFlat(resource) {
94
102
  const containedMedicationText = containedMedicationConcept?.text;
95
103
  const batch = containedMedication?.batch;
96
104
  const medicationConcept = resource.medicationCodeableConcept;
97
- const medicationCode = codingToValue(medicationConcept?.coding?.[0] || containedMedicationConcept?.coding?.[0]);
105
+ const medicationCodings = medicationConcept?.coding || containedMedicationConcept?.coding;
106
+ const medicationCode = codingListToCsv(medicationCodings);
98
107
  const medicationText = containedMedicationText || medicationConcept?.text || undefined;
99
108
  const dosage = resource.dosage?.[0];
100
109
  const repeat = dosage?.timing
@@ -119,7 +128,10 @@ export function medicationStatementFhirR4ToFlat(resource) {
119
128
  [MedicationStatementClaim.Source]: resource.informationSource?.reference,
120
129
  [MedicationStatementClaim.DosageRoute]: codingToValue(routeCoding),
121
130
  [MedicationStatementClaim.DoseQuantityValue]: doseQuantity?.value === undefined ? undefined : String(doseQuantity.value),
122
- [MedicationStatementClaim.DoseQuantityUnit]: codingToValue(doseQuantity?.system || doseQuantity?.code ? { system: doseQuantity?.system, code: doseQuantity?.code } : undefined) || doseQuantity?.unit,
131
+ [MedicationStatementClaimsFhirApiExtended.DoseQuantity]: codingToValue(doseQuantity?.system || doseQuantity?.code
132
+ ? { system: doseQuantity?.system, code: doseQuantity?.code }
133
+ : undefined),
134
+ [MedicationStatementClaim.DoseQuantityUnit]: doseQuantity?.unit,
123
135
  [MedicationStatementClaim.TimingFrequency]: repeat?.frequency === undefined ? undefined : String(repeat.frequency),
124
136
  [MedicationStatementClaim.TimingPeriod]: repeat?.period === undefined ? undefined : String(repeat.period),
125
137
  [MedicationStatementClaim.TimingPeriodUnit]: repeat?.periodUnit,
@@ -6,6 +6,11 @@ export declare function codingFromValue(value?: string): Array<{
6
6
  system?: string;
7
7
  code: string;
8
8
  }> | undefined;
9
+ /** Convert one canonical comma-separated token list into FHIR Coding entries. */
10
+ export declare function codingListFromCsv(value?: string): Array<{
11
+ system?: string;
12
+ code: string;
13
+ }> | undefined;
9
14
  export declare function codingToValue(coding?: {
10
15
  system?: string;
11
16
  code?: string;
@@ -11,6 +11,15 @@ export function codingFromValue(value) {
11
11
  return [{ code: system }];
12
12
  return system ? [{ system, code }] : [{ code }];
13
13
  }
14
+ /** Convert one canonical comma-separated token list into FHIR Coding entries. */
15
+ export function codingListFromCsv(value) {
16
+ const codings = String(value || '')
17
+ .split(',')
18
+ .map((token) => token.trim())
19
+ .filter(Boolean)
20
+ .flatMap((token) => codingFromValue(token) ?? []);
21
+ return codings.length ? codings : undefined;
22
+ }
14
23
  export function codingToValue(coding) {
15
24
  if (!coding?.code)
16
25
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhir-data-utils-ts",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "FHIR-version-neutral flat-claim and SearchParameter data utilities",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",