babelfhir-ts 1.5.19 → 1.5.21

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 (38) hide show
  1. package/out/src/cli/updateCommand.js +19 -12
  2. package/out/src/generator/core/constants.js +19 -0
  3. package/out/src/generator/core/extensionSdReader.js +99 -0
  4. package/out/src/generator/core/utils.js +97 -3
  5. package/out/src/generator/emitters/class/classTemplate.js +8 -2
  6. package/out/src/generator/emitters/class/sliceElementDefaults.js +5 -3
  7. package/out/src/generator/emitters/client/clientGenerator.js +22 -6
  8. package/out/src/generator/emitters/client/smartAuthGenerator.js +4 -1
  9. package/out/src/generator/emitters/interface/backboneSliceTyping.js +15 -4
  10. package/out/src/generator/emitters/interface/importManager.js +28 -2
  11. package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +21 -16
  12. package/out/src/generator/emitters/interface/interfaceFieldUtils.js +16 -0
  13. package/out/src/generator/emitters/interface/postProcessExtensions.js +1 -0
  14. package/out/src/generator/emitters/validator/closedSlicingValidation.js +146 -0
  15. package/out/src/generator/emitters/validator/sliceDelegation.js +450 -0
  16. package/out/src/generator/emitters/validator/sliceExtensionValidation.js +200 -35
  17. package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +130 -299
  18. package/out/src/generator/emitters/validator/sliceValidatorUtils.js +89 -16
  19. package/out/src/generator/emitters/validator/subExtensionScope.js +100 -0
  20. package/out/src/generator/emitters/validator/validatorBindingBuilder.js +113 -237
  21. package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +13 -4
  22. package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +104 -63
  23. package/out/src/generator/emitters/validator/validatorExpressions.js +54 -0
  24. package/out/src/generator/emitters/validator/validatorFieldBuilders.js +492 -46
  25. package/out/src/generator/emitters/validator/validatorGenerator.js +170 -169
  26. package/out/src/generator/emitters/validator/validatorRuntime.js +279 -0
  27. package/out/src/generator/emitters/validator/validatorTemplates.js +186 -54
  28. package/out/src/generator/emitters/zod/zodRefinementBuilder.js +29 -19
  29. package/out/src/generator/emitters/zod/zodSchemaGenerator.js +16 -1
  30. package/out/src/generator/index.js +21 -11
  31. package/out/src/generator/parser/packageParser.js +1 -8
  32. package/out/src/generator/parser/sdParser.js +1 -0
  33. package/out/src/generator/sdProcessor.js +303 -30
  34. package/out/src/generator/sdProcessorHelpers.js +1 -1
  35. package/out/src/generator/sdProcessorRequiredFields.js +120 -0
  36. package/out/src/main.js +37 -11
  37. package/package.json +3 -1
  38. package/parity-matrix.json +18 -18
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Closed slicing enforcement.
3
+ *
4
+ * `slicing.rules = 'closed'` means an instance element matching none of the declared
5
+ * slices is an error. Nothing enforced it: `slicingRules` was read in the interface
6
+ * emitter through an `as unknown as` cast and never written by any parser, so every
7
+ * slicing looked open. HL7 reports these as
8
+ * "This element does not match any known slice defined in the profile <url>
9
+ * and slicing is CLOSED: <path>"
10
+ * and they were the whole `entry` category of the remaining random-parity failures
11
+ * (davinci-dtr, kbv-erp — issue #138).
12
+ *
13
+ * Two discriminator shapes are enforced:
14
+ * - `{value, url}` on an extension slot — the member's url must be declared
15
+ * - `{type, $this}` on a choice element — the chosen type must be declared
16
+ *
17
+ * Deliberately limited to slicings introduced at an unsliced path. A slicing declared
18
+ * *inside* another slice (`Medication.extension:Kategorie.value[x]`) shares its
19
+ * unsliced name with its siblings, so the field model cannot tell the sets apart:
20
+ * merging them applies one slice's allowed types to every member and reports a
21
+ * conformant resource. Enforcing those needs slice-scoped traversal and is left off
22
+ * rather than approximated.
23
+ */
24
+ import { stripVersionFromCanonicalUrl } from '../../core/utils.js';
25
+ const CHOICE_SUFFIX = '[x]';
26
+ /** The url an instance of this extension slice carries (pinned `url` child, else profile). */
27
+ function instanceUrl(slice, fields) {
28
+ const sliceElemId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
29
+ const sliceFullId = slice.sliceName && !sliceElemId.includes(':')
30
+ ? `${sliceElemId}:${slice.sliceName}`
31
+ : sliceElemId;
32
+ for (const f of fields) {
33
+ const childId = (f.elementId || f.name).replace(/^[^.]+\./, '');
34
+ if (childId === sliceFullId + '.url' && typeof f.fixedValue === 'string')
35
+ return f.fixedValue;
36
+ }
37
+ return slice.profileUrls?.length ? stripVersionFromCanonicalUrl(slice.profileUrls[0]) : undefined;
38
+ }
39
+ /**
40
+ * The property name a choice slice occupies.
41
+ *
42
+ * A choice slice is conventionally named after the property (`valueCoding`), so the
43
+ * slice name is used when it already starts with the base property; the declared
44
+ * type is the fallback (`value[x]` + type `Coding` → `valueCoding`).
45
+ */
46
+ function choiceProperty(baseProp, slice) {
47
+ if (slice.sliceName?.startsWith(baseProp))
48
+ return slice.sliceName;
49
+ const code = slice.typeOptions?.[0]?.code ?? slice.baseTypeCode ?? slice.type;
50
+ if (!code)
51
+ return undefined;
52
+ return `${baseProp}${code.charAt(0).toUpperCase()}${code.slice(1)}`;
53
+ }
54
+ /** Classify a closed slicing, or undefined when it is not one this module enforces. */
55
+ function classify(leaf) {
56
+ if (leaf === 'extension' || leaf === 'modifierExtension')
57
+ return 'extension-url';
58
+ if (leaf.endsWith(CHOICE_SUFFIX))
59
+ return 'choice-type';
60
+ return undefined;
61
+ }
62
+ /**
63
+ * Bind `_csElem` to every element at `segments`, flattening array hops.
64
+ *
65
+ * Always the flattening walk, never a plain `at()`: the field model's array flags do
66
+ * not always agree with the instance (Medication.ingredient is 0..* yet was read as a
67
+ * single element), and `elementsAt` is correct for both shapes.
68
+ */
69
+ function walk(segments, body) {
70
+ const pathArgs = segments.map(s => `'${s}'`).join(', ');
71
+ return `\n for (const _csElem of elementsAt(resource, [${pathArgs}])) {${body}\n }`;
72
+ }
73
+ /**
74
+ * Emit closed-slicing checks for every element that introduces a closed slicing.
75
+ */
76
+ export function generateClosedSlicingValidations(fields, profileUrl, baseResourceType) {
77
+ const out = [];
78
+ const seen = new Set();
79
+ for (const field of fields) {
80
+ if (field.slicingRules !== 'closed' || field.sliceName)
81
+ continue;
82
+ // Address the slicing by its element id so slice qualifiers stay visible.
83
+ const elemId = (field.elementId || field.name).replace(/^[^.]+\./, '');
84
+ if (!elemId || seen.has(elemId))
85
+ continue;
86
+ // A slicing nested inside another slice cannot be addressed from the root.
87
+ if (elemId.includes(':'))
88
+ continue;
89
+ const segments = elemId.split('.');
90
+ const leaf = segments[segments.length - 1];
91
+ const kind = classify(leaf);
92
+ if (!kind)
93
+ continue;
94
+ // A choice segment above the leaf is not a JSON property name, so the walk
95
+ // cannot reach past it.
96
+ if (segments.slice(0, -1).some(s => s.endsWith(CHOICE_SUFFIX)))
97
+ continue;
98
+ const slices = fields.filter(f => f.sliceName && f.name === field.name);
99
+ if (slices.length === 0)
100
+ continue;
101
+ const fhirPath = `${baseResourceType ?? 'Resource'}.${elemId}`;
102
+ const profileRef = profileUrl ?? (baseResourceType ?? '');
103
+ const message = `This element does not match any known slice defined in the profile ${profileRef} and slicing is CLOSED: ${fhirPath}`;
104
+ if (kind === 'extension-url') {
105
+ const urls = slices.map(s => instanceUrl(s, fields)).filter((u) => !!u);
106
+ // Every slice must be identifiable, or a conformant member gets reported.
107
+ if (urls.length !== slices.length)
108
+ continue;
109
+ const allowed = `[${[...new Set(urls)].map(u => `"${u}"`).join(', ')}]`;
110
+ // A member with no url matches no slice either, and HL7 reports it: every
111
+ // discriminator here is `<canonical> in url`, so an absent url satisfies none
112
+ // of them. Firing only on a *present* url meant the member KBV's random
113
+ // Medication carries — `{extension:[…]}` with no url at all — went unreported
114
+ // while HL7 named all four slices it failed to match. The missing url itself
115
+ // is a separate finding (Extension.url: minimum required = 1) that both
116
+ // validators already agree on; this is the slicing violation on top of it.
117
+ out.push(`
118
+ // Closed slicing on ${fhirPath}: only the declared extension urls are allowed${walk(segments, `
119
+ const _csUrl = at(_csElem, 'url');
120
+ if (typeof _csUrl !== 'string' || !${allowed}.includes(_csUrl)) {
121
+ errors.push("${message}" + (typeof _csUrl === 'string' ? " (url = " + _csUrl + ")" : " (no url)"));
122
+ }`)}`);
123
+ seen.add(elemId);
124
+ continue;
125
+ }
126
+ // choice-type: the slot is `foo[x]`, so an instance writes `fooCoding` etc.
127
+ const baseProp = leaf.slice(0, -CHOICE_SUFFIX.length);
128
+ const props = slices.map(s => choiceProperty(baseProp, s)).filter((p) => !!p);
129
+ if (props.length !== slices.length)
130
+ continue;
131
+ const allowedProps = `[${[...new Set(props)].map(p => `"${p}"`).join(', ')}]`;
132
+ // The choice lives on the parent element — `foo[x]` is not a property name.
133
+ const body = `
134
+ const _csKey = Object.keys(node(_csElem)).find((k) => k.startsWith("${baseProp}") && k !== "${baseProp}");
135
+ if (_csKey !== undefined && !${allowedProps}.includes(_csKey)) {
136
+ errors.push("${message}.ofType(" + _csKey.slice(${baseProp.length}).toLowerCase() + ")");
137
+ }`;
138
+ const parentSegments = segments.slice(0, -1);
139
+ out.push(`
140
+ // Closed slicing on ${fhirPath}: only the declared types are allowed${parentSegments.length === 0
141
+ ? `\n {\n const _csElem = resource;${body}\n }`
142
+ : walk(parentSegments, body)}`);
143
+ seen.add(elemId);
144
+ }
145
+ return out;
146
+ }
@@ -0,0 +1,450 @@
1
+ /**
2
+ * Delegating slice members to other generated validators.
3
+ *
4
+ * A slice can pin its resource, or a reference's target, to another profile. These
5
+ * emitters match the members and hand each one to that profile's generated
6
+ * validator, which is what keeps findings inside a nested resource from being
7
+ * lost. Split out of sliceValidatorGenerator, which had grown past the size limit.
8
+ */
9
+ import { stripVersionFromCanonicalUrl } from '../../core/utils.js';
10
+ import { safeAccessor, safeList, collectWithinSliceMaxChildren, generateWithinSliceMaxChecks } from './sliceValidatorUtils.js';
11
+ export function generateProfiledChildDelegation(slice, relPath, childFieldsWithFixedValue, out, ctx) {
12
+ if (!ctx.profileUrlToName || !ctx.validatorImports)
13
+ return;
14
+ const sliceElemId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
15
+ // Find child fields of this slice that have profile URLs (profiled sub-resources)
16
+ const profiledChildren = ctx.fields.filter(f => {
17
+ const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
18
+ if (!fId.startsWith(sliceElemId + '.'))
19
+ return false;
20
+ const relativePath = fId.substring(sliceElemId.length + 1);
21
+ if (relativePath.includes('.') || relativePath.includes(':'))
22
+ return false;
23
+ return f.profileUrls && f.profileUrls.length > 0;
24
+ });
25
+ if (profiledChildren.length === 0)
26
+ return;
27
+ // Get discriminator info to build the filter expression
28
+ const discriminatorChild = childFieldsWithFixedValue[0];
29
+ const childFieldId = (discriminatorChild.elementId || discriminatorChild.name).replace(/^[^.]+\./, '');
30
+ let discriminatorProp = '';
31
+ if (childFieldId.startsWith(sliceElemId + '.')) {
32
+ discriminatorProp = childFieldId.substring(sliceElemId.length + 1).split('.')[0];
33
+ }
34
+ if (!discriminatorProp)
35
+ return;
36
+ const fixedVal = typeof discriminatorChild.fixedValue === 'string'
37
+ ? `"${discriminatorChild.fixedValue}"`
38
+ : String(discriminatorChild.fixedValue);
39
+ const sliceLabel = slice.sliceName || 'slice';
40
+ for (const profiledChild of profiledChildren) {
41
+ const childRelPath = ((profiledChild.elementId || profiledChild.name).replace(/^[^.]+\./, '')).substring(sliceElemId.length + 1);
42
+ // Skip choice-type children (value[x]) — these are datatype values (e.g., Signature),
43
+ // not nested FHIR resources. Delegation only applies to concrete resource children.
44
+ if (childRelPath.includes('[x]'))
45
+ continue;
46
+ for (const pUrl of profiledChild.profileUrls) {
47
+ const delegateName = ctx.profileUrlToName.get(pUrl);
48
+ if (!delegateName || delegateName === ctx.profileName)
49
+ continue;
50
+ ctx.validatorImports.set(delegateName, `./${delegateName}.js`);
51
+ const varName = (slice.sliceName || 'slice').replace(/[^a-zA-Z0-9_]/g, '_');
52
+ // Derive expected resourceType for the profiled child. When the child type is
53
+ // 'Resource' (generic), try the profile URL last segment or delegate name suffix.
54
+ const childType = profiledChild.baseTypeCode || profiledChild.type;
55
+ let expectedResType;
56
+ if (childType && childType !== 'Resource' && /^[A-Z][a-zA-Z]+$/.test(childType)) {
57
+ expectedResType = childType;
58
+ }
59
+ else if (ctx.profileUrlToType) {
60
+ // Use the profileUrlToType map to get the expected resource type from the profile
61
+ expectedResType = ctx.profileUrlToType.get(pUrl);
62
+ }
63
+ if (!expectedResType) {
64
+ const seg = pUrl.split('/').pop() || '';
65
+ if (/^[A-Z][a-zA-Z]+$/.test(seg))
66
+ expectedResType = seg;
67
+ }
68
+ // Build a resourceType guard to avoid false positives on wrong types
69
+ const rtGuard = expectedResType
70
+ ? ` && at(_p, '${childRelPath}', 'resourceType') === "${expectedResType}"`
71
+ : '';
72
+ // When resource type doesn't match, emit errors matching HL7 behavior:
73
+ // - Type mismatch error → field: resource
74
+ // - For Parameters: unknown resource type → field: unclassified
75
+ // - For Parameters: inv-1 (parameter must have value/resource/part) → field: constraint
76
+ const isParameters = ctx.baseResourceType === 'Parameters';
77
+ let rtMismatchCheck = '';
78
+ if (expectedResType) {
79
+ const mismatchErrors = [
80
+ `errors.push("Specified profile type was '${expectedResType}' in profile '${pUrl}', but found type '" + (at(_p, '${childRelPath}', 'resourceType') || "Resource") + "'");`,
81
+ ];
82
+ if (isParameters) {
83
+ mismatchErrors.push(`errors.push("Contained resource does not appear to be a FHIR resource (unknown name '" + at(_p, '${childRelPath}', 'resourceType') + "')");`, `errors.push("A parameter must have one and only one of (value, resource, part)");`);
84
+ }
85
+ rtMismatchCheck = `\n if (at(_p, '${discriminatorProp}') === ${fixedVal} && at(_p, '${childRelPath}') && at(_p, '${childRelPath}', 'resourceType') !== "${expectedResType}") {\n ${mismatchErrors.join('\n ')}\n }`;
86
+ }
87
+ // Detect if sliced path goes through an array ancestor (e.g., parameter.part where parameter is array)
88
+ const pathParts = relPath.split('.');
89
+ let arrayParentPath = '';
90
+ for (let i = 0; i < pathParts.length - 1; i++) {
91
+ const partialPath = pathParts.slice(0, i + 1).join('.');
92
+ const parentField = ctx.fields.find(f => {
93
+ const fRelPath = f.name.replace(/^[^.]+\./, '');
94
+ return fRelPath === partialPath;
95
+ });
96
+ if (parentField && parentField.isArray) {
97
+ arrayParentPath = partialPath;
98
+ break;
99
+ }
100
+ }
101
+ if (arrayParentPath) {
102
+ const remainingPath = relPath.substring(arrayParentPath.length + 1);
103
+ out.push(`
104
+ // Profiled sub-resource delegation for ${relPath}:${sliceLabel}.${childRelPath} → ${delegateName}
105
+ {
106
+ if (resource.${arrayParentPath} && Array.isArray(resource.${arrayParentPath})) {
107
+ for (const _parentItem of resource.${arrayParentPath}) {
108
+ const _pi = node(_parentItem);
109
+ const _nestedArr = _pi?.${remainingPath};
110
+ if (_nestedArr && Array.isArray(_nestedArr)) {
111
+ for (const _p of _nestedArr) {
112
+ if (at(_p, '${discriminatorProp}') === ${fixedVal} && at(_p, '${childRelPath}')${rtGuard}) {
113
+ const _sub = await validate${delegateName}(asResource(at(_p, '${childRelPath}')), _delegateOptions);
114
+ errors.push(..._sub.errors);
115
+ warnings.push(..._sub.warnings);
116
+ }${rtMismatchCheck ? rtMismatchCheck.replace(/\n {8}/g, '\n ') : ''}
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }`);
122
+ }
123
+ else {
124
+ out.push(`
125
+ // Profiled sub-resource delegation for ${relPath}:${sliceLabel}.${childRelPath} → ${delegateName}
126
+ {
127
+ const _${varName}Params = elements(${safeAccessor(relPath)});
128
+ for (const _p of _${varName}Params) {
129
+ if (at(_p, '${discriminatorProp}') === ${fixedVal} && at(_p, '${childRelPath}')${rtGuard}) {
130
+ const _sub = await validate${delegateName}(asResource(at(_p, '${childRelPath}')), _delegateOptions);
131
+ errors.push(..._sub.errors);
132
+ warnings.push(..._sub.warnings);
133
+ }${rtMismatchCheck}
134
+ }
135
+ }`);
136
+ }
137
+ break; // Only delegate to first resolved profile
138
+ }
139
+ }
140
+ }
141
+ // ── Reference profile discriminator support (Reference.type matching) ─────
142
+ /**
143
+ * Detect whether a slice on a Reference-typed field uses a `profile` discriminator
144
+ * on `$this.resolve()`. Returns the target resource type if discoverable.
145
+ * E.g., MessageHeader.focus:admit-notification with targetProfile → Encounter.
146
+ */
147
+ export function detectReferenceProfileDiscriminator(slice, profileUrlToType) {
148
+ if (!slice.typeOptions?.length)
149
+ return undefined;
150
+ const refType = slice.typeOptions.find(t => t.code === 'Reference');
151
+ if (!refType?.targetProfileUrls?.length)
152
+ return undefined;
153
+ const targetUrl = refType.targetProfileUrls[0];
154
+ // 1. Authoritative: resolve from profileUrlToType map
155
+ if (profileUrlToType) {
156
+ const resolved = profileUrlToType.get(targetUrl);
157
+ if (resolved && /^[A-Z][a-zA-Z]+$/.test(resolved))
158
+ return resolved;
159
+ }
160
+ // 2. Heuristic: last URL segment if it looks like a resource type
161
+ const lastSeg = targetUrl.split('/').pop() || '';
162
+ if (/^[A-Z][a-zA-Z]+$/.test(lastSeg))
163
+ return lastSeg;
164
+ return undefined;
165
+ }
166
+ /**
167
+ * Generate slice validation for Reference-typed slices discriminated by target profile.
168
+ * Matches references by `Reference.type` field or reference string path prefix.
169
+ */
170
+ /**
171
+ * Required slice discriminated by `$this` where the slice element itself carries
172
+ * `fixed[x]` — the value in the array *is* the discriminator.
173
+ *
174
+ * The canonical case is a profile pinning its own identity, which German IGs do
175
+ * on nearly every resource:
176
+ *
177
+ * Composition.meta.profile slicing: value on $this, min 1
178
+ * Composition.meta.profile:erpProfile min 1, fixedCanonical: '…|1.4'
179
+ *
180
+ * Every other strategy keys off a *child* element (pattern coding, child
181
+ * fixedValue, binding, extension url), so this shape fell through to
182
+ * "no pattern/value discriminator found" and emitted no check at all. HL7 reports
183
+ * `Slice 'Composition.meta.profile:erpProfile': a matching slice is required, but
184
+ * not found`, we reported nothing, and kbv-erp scored 0/2 on both parity metrics
185
+ * for exactly that one field.
186
+ *
187
+ * The fixed value is compared verbatim, version suffix included: a canonical
188
+ * pinned to `|1.4` is not satisfied by the unversioned URL, and HL7 agrees.
189
+ */
190
+ export function generateReferenceProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, targetResourceType, out, fields, baseResourceType) {
191
+ // Build within-slice child max constraint checks (e.g., max=0 forbidden fields)
192
+ const maxChildren = fields ? collectWithinSliceMaxChildren(slice, fields) : [];
193
+ const resType = baseResourceType || (slice.elementId || slice.name).split('.')[0];
194
+ const childMaxChecks = generateWithinSliceMaxChecks(maxChildren, `_${varName}Refs`, resType, relPath, sliceLabel);
195
+ out.push(`
196
+ // Required Reference-profile slice validation for ${relPath}:${sliceLabel} (target → ${targetResourceType})
197
+ if (Array.isArray(${safeAccessor(relPath)})) {
198
+ const _${varName}Refs = ${safeList(relPath)}.filter((e) =>
199
+ e?.type === '${targetResourceType}' ||
200
+ (typeof e?.reference === 'string' && (e.reference.startsWith('${targetResourceType}/') || new RegExp('/${targetResourceType}/').test(e.reference)))
201
+ );
202
+ if (_${varName}Refs.length < ${min}) {
203
+ errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
204
+ }${childMaxChecks}
205
+ } else if (${safeAccessor(relPath)} && !Array.isArray(${safeAccessor(relPath)})) {
206
+ const _ref = ${safeAccessor(relPath)} as { type?: string; reference?: string } | undefined;
207
+ const _isMatch = _ref?.type === '${targetResourceType}' ||
208
+ (typeof _ref?.reference === 'string' && (_ref.reference.startsWith('${targetResourceType}/') || new RegExp('/${targetResourceType}/').test(_ref.reference)));
209
+ if (!_isMatch) {
210
+ errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
211
+ }
212
+ } else {
213
+ errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
214
+ }`);
215
+ }
216
+ /**
217
+ * Detect whether a slice uses a `profile` discriminator on a `.resource` child.
218
+ * Returns the expected resourceType and profile URLs if found, undefined otherwise.
219
+ *
220
+ * Heuristic: the child field `<sliceElementId>.resource` with `profileUrls`
221
+ * indicates a profile-based entry slice. The resource type is derived from
222
+ * `profileUrlToType` (authoritative), the profile URL last segment, or the
223
+ * child field type code.
224
+ */
225
+ /**
226
+ * Delegate an optional slice's profiled resource without asserting the slice is
227
+ * present.
228
+ *
229
+ * Reuses the profile-discriminator path with min=0, which emits the member match
230
+ * and delegation but no cardinality error — the slice is allowed to be absent, and
231
+ * only its contents are in question.
232
+ */
233
+ export function generateOptionalProfiledSliceDelegation(slice, baseName, fields, out, ctx) {
234
+ const relPath = baseName.replace(/^[^.]+\./, '');
235
+ // Nested and choice-type slices need the scoping the required path builds up;
236
+ // this narrow route only covers a top-level repeating element.
237
+ if (!relPath || relPath.includes('.') || relPath.includes('[x]'))
238
+ return;
239
+ const match = detectProfileDiscriminator(slice, fields, ctx.profileUrlToType);
240
+ if (!match)
241
+ return;
242
+ const sliceLabel = slice.sliceName || 'slice';
243
+ const varName = sliceLabel.replace(/[^a-zA-Z0-9_]/g, '_');
244
+ generateProfileDiscriminatorValidation(slice, relPath, relPath, sliceLabel, varName, 0, match, out, ctx);
245
+ }
246
+ /**
247
+ * A slice discriminated by `{type, $this}` on a slot holding inline resources.
248
+ *
249
+ * cdex slices Task.contained that way, with contained:practitionerrole typed
250
+ * PractitionerRole and contained:patient typed Patient. The member *is* the
251
+ * resource, so membership is decided by its resourceType — unlike the Bundle case,
252
+ * where the type sits on the slice's `.resource` child.
253
+ *
254
+ * field.type is often the profile alias (cdex-practitionerrole), so the concrete
255
+ * type is taken from baseTypeCode or typeOptions in preference to it.
256
+ */
257
+ export function detectSelfTypeDiscriminator(slice) {
258
+ const isResourceTypeName = (name) => !!name && /^[A-Z][A-Za-z]+$/.test(name) && name !== 'Resource' && name !== 'DomainResource';
259
+ const candidates = [
260
+ slice.baseTypeCode,
261
+ ...(slice.typeOptions ?? []).map(option => option.code),
262
+ slice.type,
263
+ ];
264
+ return candidates.find(isResourceTypeName);
265
+ }
266
+ /**
267
+ * Emit the cardinality check for a `{type, $this}` slice.
268
+ *
269
+ * Deliberately does not delegate to the slice's profile: that would validate the
270
+ * contained resource against it and add findings whose agreement with HL7 is
271
+ * unverified. This covers the missing membership check only.
272
+ */
273
+ export function generateSelfTypeSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, resourceType, out) {
274
+ void slice;
275
+ out.push(`
276
+ // Required slice validation for ${relPath}:${sliceLabel} (type discriminator → ${resourceType})
277
+ {
278
+ const _${varName}Typed = elements(${safeAccessor(relPath)}).filter(
279
+ (item) => at(item, 'resourceType') === '${resourceType}'
280
+ );
281
+ if (_${varName}Typed.length < ${min}) {
282
+ errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
283
+ }
284
+ }`);
285
+ }
286
+ /**
287
+ * Emit the cardinality check for a slice discriminated by `{profile, $this}` against
288
+ * a datatype profile.
289
+ *
290
+ * The slice names a profile rather than a value, so the profile's pinned element is
291
+ * what distinguishes its members — sdc's useContext:library resolves to
292
+ * UsageContext.code pinned to usage-context-type|workflow. Without this the slice
293
+ * was unmatchable and HL7's missing-slice finding had no counterpart.
294
+ */
295
+ export function generateDatatypeProfileSliceValidation(relPath, errorPath, sliceLabel, varName, min, pin, out) {
296
+ const target = `at(item, '${pin.path}')`;
297
+ // A Coding carries system/code itself; a CodeableConcept wraps them in .coding.
298
+ const predicate = pin.shape === 'coding'
299
+ ? [
300
+ pin.system ? `coding(${target}).system === '${pin.system}'` : '',
301
+ `coding(${target}).code === '${pin.code}'`,
302
+ ].filter(Boolean).join(' && ')
303
+ : `codings(${target}).some((c) => ${[
304
+ pin.system ? `c.system === '${pin.system}'` : '',
305
+ `c.code === '${pin.code}'`,
306
+ ].filter(Boolean).join(' && ')})`;
307
+ out.push(`
308
+ // Required slice validation for ${relPath}:${sliceLabel} (profile discriminator → ${pin.path} pinned to ${pin.code})
309
+ {
310
+ const _${varName}Profiled = elements(${safeAccessor(relPath)}).filter((item) => ${predicate});
311
+ if (_${varName}Profiled.length < ${min}) {
312
+ errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
313
+ }
314
+ }`);
315
+ }
316
+ export function detectProfileDiscriminator(slice, fields, profileUrlToType) {
317
+ const sliceElemId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
318
+ const resourceChild = fields.find(f => {
319
+ const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
320
+ return fId === `${sliceElemId}.resource`;
321
+ });
322
+ if (!resourceChild?.profileUrls?.length)
323
+ return undefined;
324
+ const profileUrls = resourceChild.profileUrls;
325
+ // 1. Authoritative: resolve resource type from profileUrlToType map
326
+ if (profileUrlToType) {
327
+ for (const pUrl of profileUrls) {
328
+ const resolvedType = profileUrlToType.get(pUrl);
329
+ if (resolvedType && /^[A-Z][a-zA-Z]+$/.test(resolvedType)) {
330
+ return { resourceType: resolvedType, profileUrls };
331
+ }
332
+ }
333
+ }
334
+ // 2. Try to extract from profile URL last segment (core profiles)
335
+ const profileUrl = profileUrls[0];
336
+ const lastSegment = profileUrl.split('/').pop() || '';
337
+ if (/^[A-Z][a-zA-Z]+$/.test(lastSegment)) {
338
+ return { resourceType: lastSegment, profileUrls };
339
+ }
340
+ // 3. Slice name if it looks like a resource type
341
+ const sliceName = slice.sliceName || '';
342
+ if (/^[A-Z][a-zA-Z]+$/.test(sliceName)) {
343
+ return { resourceType: sliceName, profileUrls };
344
+ }
345
+ // 4. Fallback: derive from the child's type code or typeOptions
346
+ const childType = resourceChild.baseTypeCode || resourceChild.type;
347
+ if (childType && /^[A-Z][a-zA-Z]+$/.test(childType)) {
348
+ return { resourceType: childType, profileUrls };
349
+ }
350
+ const typeOpt = resourceChild.typeOptions?.find(t => /^[A-Z][a-zA-Z]+$/.test(t.code));
351
+ if (typeOpt) {
352
+ return { resourceType: typeOpt.code, profileUrls };
353
+ }
354
+ return undefined;
355
+ }
356
+ /**
357
+ * Generate slice validation for profile-discriminated entry slices.
358
+ * Matches Bundle entries by `entry[i].resource.resourceType`, enforces min/max,
359
+ * and delegates to the profiled validator when the profile URL is in profileUrlToName.
360
+ */
361
+ export function generateProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, match, out, ctx) {
362
+ const { resourceType, profileUrls } = match;
363
+ const max = typeof slice.max === 'number' && slice.max !== Infinity ? slice.max : undefined;
364
+ const parentPath = relPath.includes('.') ? relPath.split('.').slice(0, -1).join('.') : '';
365
+ const accessor = parentPath ? `at(resource, ${parentPath.split('.').map(s => `'${s}'`).join(', ')})` : `resource.${relPath}`;
366
+ // For top-level entry slices, access resource.entry directly
367
+ const arrayAccessor = relPath === 'entry' || !relPath.includes('.') ? `resource.${relPath}` : accessor;
368
+ // Resolve profiled validator for delegation
369
+ let delegateInterfaceName;
370
+ const delegateNames = [];
371
+ if (ctx.profileUrlToName && ctx.validatorImports) {
372
+ for (const pUrl of profileUrls) {
373
+ // A type.profile on an entry slice is often version-pinned
374
+ // (…/dtr-questionnaireresponse|2.2.0) while the map is keyed by the bare
375
+ // canonical, so the versioned form has to fall back to the stripped one.
376
+ // Without it the slice emitted its cardinality check but never delegated,
377
+ // and every finding inside the entry's own profile went unreported.
378
+ const name = ctx.profileUrlToName.get(pUrl)
379
+ ?? ctx.profileUrlToName.get(stripVersionFromCanonicalUrl(pUrl));
380
+ if (name && name !== ctx.profileName) {
381
+ if (!delegateInterfaceName)
382
+ delegateInterfaceName = name;
383
+ // Every candidate is collected, not just the first: an entry slice may allow
384
+ // several profiles, and conformance to any one of them is enough.
385
+ if (!delegateNames.includes(name)) {
386
+ delegateNames.push(name);
387
+ ctx.validatorImports.set(name, `./${name}.js`);
388
+ }
389
+ }
390
+ }
391
+ }
392
+ // When delegating, annotate the array so .resource is accessible without casts
393
+ const entriesType = '';
394
+ let code = `
395
+ // Required slice validation for ${relPath}:${sliceLabel} (profile discriminator → ${resourceType})
396
+ {
397
+ const _${varName}Entries${entriesType} = elements(${arrayAccessor}).filter(
398
+ (e) => at(e, 'resource', 'resourceType') === '${resourceType}'
399
+ );`;
400
+ if (min > 0) {
401
+ code += `
402
+ if (_${varName}Entries.length < ${min}) {
403
+ errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
404
+ }`;
405
+ }
406
+ if (max !== undefined) {
407
+ code += `
408
+ if (_${varName}Entries.length > ${max}) {
409
+ errors.push("${errorPath}:${sliceLabel}: max allowed = ${max}, but found " + _${varName}Entries.length);
410
+ }`;
411
+ }
412
+ // Delegate to profiled validator for each matched entry
413
+ if (delegateNames.length > 1) {
414
+ // Several profiles are allowed, so conforming to any one of them is enough.
415
+ // Reporting the first candidate's findings unconditionally invented errors for an
416
+ // entry that legitimately matched a later choice. When none matches, HL7 adds two
417
+ // summaries alongside the individual findings, which is mirrored here.
418
+ const choices = profileUrls.join(', ');
419
+ code += `
420
+ for (const _e of _${varName}Entries) {
421
+ if (at(_e, 'resource')) {
422
+ const _${varName}Candidates = [
423
+ ${delegateNames.map(name => ` await validate${name}(asResource(at(_e, 'resource')), _delegateOptions),`).join('\n')}
424
+ ];
425
+ const _${varName}Conforming = _${varName}Candidates.find((c) => c.errors.length === 0);
426
+ if (_${varName}Conforming) {
427
+ warnings.push(..._${varName}Conforming.warnings);
428
+ } else {
429
+ errors.push("The entry resource did not match any of the allowed profiles (Type ${resourceType}: ${choices})");
430
+ errors.push("Unable to find a match for the specified profile among choices: ${choices}");
431
+ errors.push(..._${varName}Candidates[0].errors);
432
+ warnings.push(..._${varName}Candidates[0].warnings);
433
+ }
434
+ }
435
+ }`;
436
+ }
437
+ else if (delegateInterfaceName) {
438
+ code += `
439
+ for (const _e of _${varName}Entries) {
440
+ if (at(_e, 'resource')) {
441
+ const _sub = await validate${delegateInterfaceName}(asResource(at(_e, 'resource')), _delegateOptions);
442
+ errors.push(..._sub.errors);
443
+ warnings.push(..._sub.warnings);
444
+ }
445
+ }`;
446
+ }
447
+ code += `
448
+ }`;
449
+ out.push(code);
450
+ }