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
@@ -4,7 +4,9 @@
4
4
  * (patternCodeableConcept, patternCoding, BackboneElement, binding, ValueSet).
5
5
  */
6
6
  import { addDiagnostic } from '../../core/sdDiagnostics.js';
7
- import { safeAccessor, safeList, sliceElseBranch, rewritePrimitiveExtensionPath, isArrayPath, collectWithinSliceMaxChildren, generateWithinSliceMaxChecks } from './sliceValidatorUtils.js';
7
+ import { INLINE_RESOURCE_SLOTS } from './validatorFieldBuilders.js';
8
+ import { safeAccessor, safeList, sliceElseBranch, rewritePrimitiveExtensionPath, isArrayPath, resolveNestedPatternChild } from './sliceValidatorUtils.js';
9
+ import { generateProfiledChildDelegation, detectReferenceProfileDiscriminator, generateReferenceProfileDiscriminatorValidation, generateOptionalProfiledSliceDelegation, detectProfileDiscriminator, generateProfileDiscriminatorValidation, detectSelfTypeDiscriminator, generateSelfTypeSliceValidation, generateDatatypeProfileSliceValidation, } from './sliceDelegation.js';
8
10
  import { generateBackboneElementValidation, generateBindingCodesValidation, generateFixedValueSliceValidation } from './sliceBackboneValidation.js';
9
11
  import { generateValueSetValidation, detectExistsExtensionDiscriminator, generateExistsExtensionValidation, detectExtensionUrlSlice, generateExtensionUrlSliceValidation, } from './sliceExtensionValidation.js';
10
12
  /**
@@ -29,8 +31,37 @@ export function generateSliceValidations(ctx) {
29
31
  slicesByBase.forEach((slices, baseName) => {
30
32
  slices.forEach(slice => {
31
33
  const min = typeof slice.min === "number" ? slice.min : (slice.isOptional ? 0 : 1);
32
- // Only validate required slices (min > 0)
33
- if (min > 0) {
34
+ // An optional extension slice is still validated when an instance is present.
35
+ // Only this shape is admitted below at min=0, mirroring the isExtensionUrlOnly
36
+ // test inside the body, so every other slice kind keeps its existing gate and
37
+ // no other emitter is reached with a count it cannot assert.
38
+ const isOptionalExtensionUrlSlice = min === 0 && isUrlDiscriminatedExtensionSlice(slice, baseName, fields);
39
+ // An optional slice still needs its profiled resource validated. HL7 checks a
40
+ // present entry whether or not the slice itself is required, so skipping the
41
+ // whole slice on min=0 lost every finding inside it — DTR's qpackage output
42
+ // Parameters pins parameter:packagebundle.resource to a Bundle profile with
43
+ // min=0, and none of that Bundle's requirements were being checked.
44
+ if (min === 0) {
45
+ const emittedBefore = sliceValidations.length;
46
+ generateOptionalProfiledSliceDelegation(slice, baseName, fields, sliceValidations, ctx);
47
+ const alreadyDelegated = sliceValidations.length > emittedBefore;
48
+ // An extension slice carries on into the body, where the url discriminator
49
+ // branch emits its value[x] type and max=0 checks. Returning here left
50
+ // DTRStdQuestionnaire's item.extension:contextExpression unvalidated: it is
51
+ // min=0, so nothing about a present instance was ever checked.
52
+ //
53
+ // Only when the delegation emitted nothing, though. A top-level extension
54
+ // slice naming a profile gets a discriminator validation from the call above,
55
+ // and continuing into the body added a second check over the same slice — so
56
+ // one finding was reported twice and parity broke on both empty and random
57
+ // resources across us-core, mcode, ndh and pas. Falling through is for the
58
+ // slices the delegation declines, which is why the earlier attempt looked
59
+ // safe on the profiles I sampled: theirs are nested, and
60
+ // generateOptionalProfiledSliceDelegation returns early on a dotted path.
61
+ if (!isOptionalExtensionUrlSlice || alreadyDelegated)
62
+ return;
63
+ }
64
+ if (min > 0 || isOptionalExtensionUrlSlice) {
34
65
  let relPath = baseName.replace(/^[^.]+\./, "");
35
66
  // Skip value[x] slices that are nested within parent elements.
36
67
  if (relPath.includes('.') && relPath.includes('value[x]')) {
@@ -68,7 +99,10 @@ export function generateSliceValidations(ctx) {
68
99
  ('system' in slice.patternConstraint || 'code' in slice.patternConstraint) &&
69
100
  !('coding' in slice.patternConstraint);
70
101
  const sliceElementId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
71
- const childFieldsWithPattern = fields.filter(f => {
102
+ // A slice pinning a coded child discriminates on that pin, which takes
103
+ // precedence over the child's own (wider) binding.
104
+ const nestedPatternChild = resolveNestedPatternChild(slice);
105
+ const childFieldsWithPattern = nestedPatternChild ? [nestedPatternChild] : fields.filter(f => {
72
106
  if (!f.elementId)
73
107
  return false;
74
108
  const hasPattern = !!f.patternConstraint;
@@ -112,7 +146,7 @@ export function generateSliceValidations(ctx) {
112
146
  const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
113
147
  return fId.substring(sliceElemId.length + 1).split('.')[0];
114
148
  });
115
- generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel, varName, min, fields, sliceValidations, requiredSubFields);
149
+ generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel, varName, min, fields, sliceValidations, requiredSubFields, ctx.baseResourceType, ctx.profileUrl);
116
150
  }
117
151
  else if (childFieldsWithFixedValue.length > 0) {
118
152
  // For extension slices where the only fixedValue child is 'url', prefer the
@@ -164,6 +198,19 @@ export function generateSliceValidations(ctx) {
164
198
  if (refResourceType) {
165
199
  generateReferenceProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, refResourceType, sliceValidations, fields, ctx.baseResourceType);
166
200
  }
201
+ else if (slice.fixedValue !== undefined && !isChoiceTypeSlice) {
202
+ generateSelfFixedValueSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, sliceValidations);
203
+ }
204
+ else if (slice.profileUrls?.length && ctx.datatypeProfilePins?.get(slice.profileUrls[0])) {
205
+ // A {profile, $this} slice against a datatype profile: the profile's
206
+ // own pinned element is what distinguishes its members.
207
+ generateDatatypeProfileSliceValidation(relPath, errorPath, sliceLabel, varName, min, ctx.datatypeProfilePins.get(slice.profileUrls[0]), sliceValidations);
208
+ }
209
+ else if (INLINE_RESOURCE_SLOTS.has(relPath) && detectSelfTypeDiscriminator(slice)) {
210
+ // An inline-resource slot sliced by {type, $this}: the member is the
211
+ // resource, so its resourceType decides membership.
212
+ generateSelfTypeSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, detectSelfTypeDiscriminator(slice), sliceValidations);
213
+ }
167
214
  else {
168
215
  sliceValidations.push(`
169
216
  // Slice validation for ${relPath}:${sliceLabel} skipped: no pattern/value discriminator found
@@ -234,16 +281,19 @@ function generateCrossSliceExclusivityValidations(slicesByBase, fields, fieldPat
234
281
  return;
235
282
  if (!isArrayPath(relPath, fieldPathMap, arrayFieldPaths))
236
283
  return;
284
+ // `elements()`, not the raw accessor: a multi-segment path compiles to
285
+ // `at(resource, …)`, which is `unknown`, and an enclosing `Array.isArray()`
286
+ // does not narrow a fresh call expression — so `for…of` on it fails to
287
+ // typecheck. The matchers read `_el.coding`, so the list must stay element-
288
+ // typed rather than going through `arr()`.
237
289
  let code = `\n // Cross-slice exclusivity validation for ${relPath}`;
238
- code += `\n if (Array.isArray(${safeAccessor(relPath)})) {`;
239
- code += `\n for (const _el of ${safeAccessor(relPath)}) {`;
240
- code += `\n const _matchedSlices: string[] = [];`;
290
+ code += `\n for (const _el of ${safeList(relPath)}) {`;
291
+ code += `\n const _matchedSlices: string[] = [];`;
241
292
  for (const m of matchers) {
242
- code += `\n if (${m.matchExpr}) _matchedSlices.push("${m.label}");`;
293
+ code += `\n if (${m.matchExpr}) _matchedSlices.push("${m.label}");`;
243
294
  }
244
- code += `\n if (_matchedSlices.length > 1) {`;
245
- code += `\n errors.push("Element matches more than one slice - " + _matchedSlices.join(", "));`;
246
- code += `\n }`;
295
+ code += `\n if (_matchedSlices.length > 1) {`;
296
+ code += `\n errors.push("Element matches more than one slice - " + _matchedSlices.join(", "));`;
247
297
  code += `\n }`;
248
298
  code += `\n }`;
249
299
  out.push(code);
@@ -384,7 +434,7 @@ function generatePatternCodeableConceptValidation(slice, relPath, errorPath, sli
384
434
  }`);
385
435
  }
386
436
  }
387
- function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel, varName, min, fields, out, requiredSubFields) {
437
+ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel, varName, min, fields, out, requiredSubFields, baseResourceType, profileUrl) {
388
438
  const pattern = slice.patternConstraint;
389
439
  const hasCode = 'code' in pattern;
390
440
  const hasSystem = 'system' in pattern;
@@ -404,6 +454,40 @@ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel,
404
454
  }`)
405
455
  .join('')
406
456
  : '';
457
+ // Fixed values pinned on the slice's own children.
458
+ //
459
+ // This validator only ever checked that a required child was *present*, so a
460
+ // pinned value went unenforced: KBV_PR_ERP_Medication_Compounding fixes
461
+ // code.coding:verordnungskategorieCode.code to 'rezeptur' and we reported
462
+ // nothing where HL7 reports the mismatch. The CodeableConcept sibling has done
463
+ // this for its descendants all along — the gap was only here.
464
+ const sliceIdRel = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
465
+ const resType = baseResourceType || sliceIdRel.split('.')[0];
466
+ const fixedChildChecks = fields
467
+ .filter(f => {
468
+ if (typeof f.fixedValue !== 'string' && typeof f.fixedValue !== 'number' && f.fixedValue !== true)
469
+ return false;
470
+ const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
471
+ if (!fId.startsWith(sliceIdRel + '.'))
472
+ return false;
473
+ const rel = fId.substring(sliceIdRel.length + 1);
474
+ // Direct children only; a deeper pin needs per-level descent this loop
475
+ // does not do, and a sub-slice has its own validator.
476
+ return !rel.includes('.') && !rel.includes(':');
477
+ })
478
+ .map(f => {
479
+ const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
480
+ const child = fId.substring(sliceIdRel.length + 1);
481
+ const expected = String(f.fixedValue).replace(/'/g, "\\'");
482
+ // The fragment must end at the pinned child so parity attributes this to
483
+ // the same field HL7 does.
484
+ const profileRef = `${profileUrl ? `${profileUrl}#` : ''}${resType}.${sliceIdRel}.${child}`;
485
+ return `
486
+ if ((matched as Record<string, unknown>).${child} !== undefined && String((matched as Record<string, unknown>).${child}) !== '${expected}') {
487
+ errors.push("Value is '" + (matched as Record<string, unknown>).${child} + "' but is fixed to '${expected}' in the profile ${profileRef}");
488
+ }`;
489
+ })
490
+ .join('');
407
491
  const pathParts = relPath.split('.');
408
492
  let isNestedInArray = false;
409
493
  let arrayParentPath = '';
@@ -437,8 +521,9 @@ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel,
437
521
  }
438
522
  }
439
523
  }
440
- const subFieldBlock = subFieldChecks ? `
441
- for (const matched of ${varName}Elements) {${subFieldChecks}
524
+ const matchedChecks = subFieldChecks + fixedChildChecks;
525
+ const subFieldBlock = matchedChecks ? `
526
+ for (const matched of ${varName}Elements) {${matchedChecks}
442
527
  }` : '';
443
528
  if (isNestedInArray && arrayParentPath) {
444
529
  const remainingPath = relPath.substring(arrayParentPath.length + 1);
@@ -522,296 +607,42 @@ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel,
522
607
  * generates code that delegates validation of that sub-resource to the profiled
523
608
  * validator, similar to how generateProfileDiscriminatorValidation handles Bundle entries.
524
609
  */
525
- function generateProfiledChildDelegation(slice, relPath, childFieldsWithFixedValue, out, ctx) {
526
- if (!ctx.profileUrlToName || !ctx.validatorImports)
610
+ function generateSelfFixedValueSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, out) {
611
+ const fixed = slice.fixedValue;
612
+ if (fixed === undefined)
527
613
  return;
528
- const sliceElemId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
529
- // Find child fields of this slice that have profile URLs (profiled sub-resources)
530
- const profiledChildren = ctx.fields.filter(f => {
531
- const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
532
- if (!fId.startsWith(sliceElemId + '.'))
533
- return false;
534
- const relativePath = fId.substring(sliceElemId.length + 1);
535
- if (relativePath.includes('.') || relativePath.includes(':'))
536
- return false;
537
- return f.profileUrls && f.profileUrls.length > 0;
538
- });
539
- if (profiledChildren.length === 0)
540
- return;
541
- // Get discriminator info to build the filter expression
542
- const discriminatorChild = childFieldsWithFixedValue[0];
543
- const childFieldId = (discriminatorChild.elementId || discriminatorChild.name).replace(/^[^.]+\./, '');
544
- let discriminatorProp = '';
545
- if (childFieldId.startsWith(sliceElemId + '.')) {
546
- discriminatorProp = childFieldId.substring(sliceElemId.length + 1).split('.')[0];
547
- }
548
- if (!discriminatorProp)
549
- return;
550
- const fixedVal = typeof discriminatorChild.fixedValue === 'string'
551
- ? `"${discriminatorChild.fixedValue}"`
552
- : String(discriminatorChild.fixedValue);
553
- const sliceLabel = slice.sliceName || 'slice';
554
- for (const profiledChild of profiledChildren) {
555
- const childRelPath = ((profiledChild.elementId || profiledChild.name).replace(/^[^.]+\./, '')).substring(sliceElemId.length + 1);
556
- // Skip choice-type children (value[x]) — these are datatype values (e.g., Signature),
557
- // not nested FHIR resources. Delegation only applies to concrete resource children.
558
- if (childRelPath.includes('[x]'))
559
- continue;
560
- for (const pUrl of profiledChild.profileUrls) {
561
- const delegateName = ctx.profileUrlToName.get(pUrl);
562
- if (!delegateName || delegateName === ctx.profileName)
563
- continue;
564
- ctx.validatorImports.set(delegateName, `./${delegateName}.js`);
565
- const varName = (slice.sliceName || 'slice').replace(/[^a-zA-Z0-9_]/g, '_');
566
- // Derive expected resourceType for the profiled child. When the child type is
567
- // 'Resource' (generic), try the profile URL last segment or delegate name suffix.
568
- const childType = profiledChild.baseTypeCode || profiledChild.type;
569
- let expectedResType;
570
- if (childType && childType !== 'Resource' && /^[A-Z][a-zA-Z]+$/.test(childType)) {
571
- expectedResType = childType;
572
- }
573
- else if (ctx.profileUrlToType) {
574
- // Use the profileUrlToType map to get the expected resource type from the profile
575
- expectedResType = ctx.profileUrlToType.get(pUrl);
576
- }
577
- if (!expectedResType) {
578
- const seg = pUrl.split('/').pop() || '';
579
- if (/^[A-Z][a-zA-Z]+$/.test(seg))
580
- expectedResType = seg;
581
- }
582
- // Build a resourceType guard to avoid false positives on wrong types
583
- const rtGuard = expectedResType
584
- ? ` && at(_p, '${childRelPath}', 'resourceType') === "${expectedResType}"`
585
- : '';
586
- // When resource type doesn't match, emit errors matching HL7 behavior:
587
- // - Type mismatch error → field: resource
588
- // - For Parameters: unknown resource type → field: unclassified
589
- // - For Parameters: inv-1 (parameter must have value/resource/part) → field: constraint
590
- const isParameters = ctx.baseResourceType === 'Parameters';
591
- let rtMismatchCheck = '';
592
- if (expectedResType) {
593
- const mismatchErrors = [
594
- `errors.push("Specified profile type was '${expectedResType}' in profile '${pUrl}', but found type '" + (at(_p, '${childRelPath}', 'resourceType') || "Resource") + "'");`,
595
- ];
596
- if (isParameters) {
597
- 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)");`);
598
- }
599
- rtMismatchCheck = `\n if (at(_p, '${discriminatorProp}') === ${fixedVal} && at(_p, '${childRelPath}') && at(_p, '${childRelPath}', 'resourceType') !== "${expectedResType}") {\n ${mismatchErrors.join('\n ')}\n }`;
600
- }
601
- // Detect if sliced path goes through an array ancestor (e.g., parameter.part where parameter is array)
602
- const pathParts = relPath.split('.');
603
- let arrayParentPath = '';
604
- for (let i = 0; i < pathParts.length - 1; i++) {
605
- const partialPath = pathParts.slice(0, i + 1).join('.');
606
- const parentField = ctx.fields.find(f => {
607
- const fRelPath = f.name.replace(/^[^.]+\./, '');
608
- return fRelPath === partialPath;
609
- });
610
- if (parentField && parentField.isArray) {
611
- arrayParentPath = partialPath;
612
- break;
613
- }
614
- }
615
- if (arrayParentPath) {
616
- const remainingPath = relPath.substring(arrayParentPath.length + 1);
617
- out.push(`
618
- // Profiled sub-resource delegation for ${relPath}:${sliceLabel}.${childRelPath} → ${delegateName}
619
- {
620
- if (resource.${arrayParentPath} && Array.isArray(resource.${arrayParentPath})) {
621
- for (const _parentItem of resource.${arrayParentPath}) {
622
- const _pi = node(_parentItem);
623
- const _nestedArr = _pi?.${remainingPath};
624
- if (_nestedArr && Array.isArray(_nestedArr)) {
625
- for (const _p of _nestedArr) {
626
- if (at(_p, '${discriminatorProp}') === ${fixedVal} && at(_p, '${childRelPath}')${rtGuard}) {
627
- const _sub = await validate${delegateName}(asResource(at(_p, '${childRelPath}')), options);
628
- errors.push(..._sub.errors);
629
- warnings.push(..._sub.warnings);
630
- }${rtMismatchCheck ? rtMismatchCheck.replace(/\n {8}/g, '\n ') : ''}
631
- }
632
- }
633
- }
634
- }
635
- }`);
636
- }
637
- else {
638
- out.push(`
639
- // Profiled sub-resource delegation for ${relPath}:${sliceLabel}.${childRelPath} → ${delegateName}
640
- {
641
- const _${varName}Params = elements(${safeAccessor(relPath)});
642
- for (const _p of _${varName}Params) {
643
- if (at(_p, '${discriminatorProp}') === ${fixedVal} && at(_p, '${childRelPath}')${rtGuard}) {
644
- const _sub = await validate${delegateName}(asResource(at(_p, '${childRelPath}')), options);
645
- errors.push(..._sub.errors);
646
- warnings.push(..._sub.warnings);
647
- }${rtMismatchCheck}
648
- }
649
- }`);
650
- }
651
- break; // Only delegate to first resolved profile
652
- }
653
- }
654
- }
655
- // ── Reference profile discriminator support (Reference.type matching) ─────
656
- /**
657
- * Detect whether a slice on a Reference-typed field uses a `profile` discriminator
658
- * on `$this.resolve()`. Returns the target resource type if discoverable.
659
- * E.g., MessageHeader.focus:admit-notification with targetProfile → Encounter.
660
- */
661
- function detectReferenceProfileDiscriminator(slice, profileUrlToType) {
662
- if (!slice.typeOptions?.length)
663
- return undefined;
664
- const refType = slice.typeOptions.find(t => t.code === 'Reference');
665
- if (!refType?.targetProfileUrls?.length)
666
- return undefined;
667
- const targetUrl = refType.targetProfileUrls[0];
668
- // 1. Authoritative: resolve from profileUrlToType map
669
- if (profileUrlToType) {
670
- const resolved = profileUrlToType.get(targetUrl);
671
- if (resolved && /^[A-Z][a-zA-Z]+$/.test(resolved))
672
- return resolved;
673
- }
674
- // 2. Heuristic: last URL segment if it looks like a resource type
675
- const lastSeg = targetUrl.split('/').pop() || '';
676
- if (/^[A-Z][a-zA-Z]+$/.test(lastSeg))
677
- return lastSeg;
678
- return undefined;
679
- }
680
- /**
681
- * Generate slice validation for Reference-typed slices discriminated by target profile.
682
- * Matches references by `Reference.type` field or reference string path prefix.
683
- */
684
- function generateReferenceProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, targetResourceType, out, fields, baseResourceType) {
685
- // Build within-slice child max constraint checks (e.g., max=0 forbidden fields)
686
- const maxChildren = fields ? collectWithinSliceMaxChildren(slice, fields) : [];
687
- const resType = baseResourceType || (slice.elementId || slice.name).split('.')[0];
688
- const childMaxChecks = generateWithinSliceMaxChecks(maxChildren, `_${varName}Refs`, resType, relPath, sliceLabel);
614
+ // The sliced element is a primitive here ($this is the value), so a plain
615
+ // equality count over arr() is the whole check — no element shape to inspect.
616
+ const literal = JSON.stringify(fixed);
689
617
  out.push(`
690
- // Required Reference-profile slice validation for ${relPath}:${sliceLabel} (target → ${targetResourceType})
691
- if (Array.isArray(${safeAccessor(relPath)})) {
692
- const _${varName}Refs = ${safeList(relPath)}.filter((e) =>
693
- e?.type === '${targetResourceType}' ||
694
- (typeof e?.reference === 'string' && (e.reference.startsWith('${targetResourceType}/') || new RegExp('/${targetResourceType}/').test(e.reference)))
695
- );
696
- if (_${varName}Refs.length < ${min}) {
697
- errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
698
- }${childMaxChecks}
699
- } else if (${safeAccessor(relPath)} && !Array.isArray(${safeAccessor(relPath)})) {
700
- const _ref = ${safeAccessor(relPath)} as { type?: string; reference?: string } | undefined;
701
- const _isMatch = _ref?.type === '${targetResourceType}' ||
702
- (typeof _ref?.reference === 'string' && (_ref.reference.startsWith('${targetResourceType}/') || new RegExp('/${targetResourceType}/').test(_ref.reference)));
703
- if (!_isMatch) {
704
- errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
705
- }
706
- } else {
618
+ // Required slice validation for ${relPath}:${sliceLabel} ($this fixed value)
619
+ if (arr(${safeAccessor(relPath)}).filter((item) => item === ${literal}).length < ${min}) {
707
620
  errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
708
621
  }`);
709
622
  }
710
623
  /**
711
- * Detect whether a slice uses a `profile` discriminator on a `.resource` child.
712
- * Returns the expected resourceType and profile URLs if found, undefined otherwise.
624
+ * Whether a slice's only pinned direct child is `url` — the shape of an extension
625
+ * slice discriminated by its canonical.
713
626
  *
714
- * Heuristic: the child field `<sliceElementId>.resource` with `profileUrls`
715
- * indicates a profile-based entry slice. The resource type is derived from
716
- * `profileUrlToType` (authoritative), the profile URL last segment, or the
717
- * child field type code.
627
+ * Deliberately identical to the isExtensionUrlOnly test inside the generator body,
628
+ * so that admitting a min=0 slice past the required-slice gate admits exactly the
629
+ * ones that then take the extension-url branch. Any other slice kind reaching a
630
+ * count-based emitter with min=0 would produce an assertion that cannot fire.
718
631
  */
719
- function detectProfileDiscriminator(slice, fields, profileUrlToType) {
720
- const sliceElemId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
721
- const resourceChild = fields.find(f => {
722
- const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
723
- return fId === `${sliceElemId}.resource`;
724
- });
725
- if (!resourceChild?.profileUrls?.length)
726
- return undefined;
727
- const profileUrls = resourceChild.profileUrls;
728
- // 1. Authoritative: resolve resource type from profileUrlToType map
729
- if (profileUrlToType) {
730
- for (const pUrl of profileUrls) {
731
- const resolvedType = profileUrlToType.get(pUrl);
732
- if (resolvedType && /^[A-Z][a-zA-Z]+$/.test(resolvedType)) {
733
- return { resourceType: resolvedType, profileUrls };
734
- }
735
- }
736
- }
737
- // 2. Try to extract from profile URL last segment (core profiles)
738
- const profileUrl = profileUrls[0];
739
- const lastSegment = profileUrl.split('/').pop() || '';
740
- if (/^[A-Z][a-zA-Z]+$/.test(lastSegment)) {
741
- return { resourceType: lastSegment, profileUrls };
742
- }
743
- // 3. Slice name if it looks like a resource type
744
- const sliceName = slice.sliceName || '';
745
- if (/^[A-Z][a-zA-Z]+$/.test(sliceName)) {
746
- return { resourceType: sliceName, profileUrls };
747
- }
748
- // 4. Fallback: derive from the child's type code or typeOptions
749
- const childType = resourceChild.baseTypeCode || resourceChild.type;
750
- if (childType && /^[A-Z][a-zA-Z]+$/.test(childType)) {
751
- return { resourceType: childType, profileUrls };
752
- }
753
- const typeOpt = resourceChild.typeOptions?.find(t => /^[A-Z][a-zA-Z]+$/.test(t.code));
754
- if (typeOpt) {
755
- return { resourceType: typeOpt.code, profileUrls };
756
- }
757
- return undefined;
758
- }
759
- /**
760
- * Generate slice validation for profile-discriminated entry slices.
761
- * Matches Bundle entries by `entry[i].resource.resourceType`, enforces min/max,
762
- * and delegates to the profiled validator when the profile URL is in profileUrlToName.
763
- */
764
- function generateProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, match, out, ctx) {
765
- const { resourceType, profileUrls } = match;
766
- const max = typeof slice.max === 'number' && slice.max !== Infinity ? slice.max : undefined;
767
- const parentPath = relPath.includes('.') ? relPath.split('.').slice(0, -1).join('.') : '';
768
- const accessor = parentPath ? `at(resource, ${parentPath.split('.').map(s => `'${s}'`).join(', ')})` : `resource.${relPath}`;
769
- // For top-level entry slices, access resource.entry directly
770
- const arrayAccessor = relPath === 'entry' || !relPath.includes('.') ? `resource.${relPath}` : accessor;
771
- // Resolve profiled validator for delegation
772
- let delegateInterfaceName;
773
- if (ctx.profileUrlToName && ctx.validatorImports) {
774
- for (const pUrl of profileUrls) {
775
- const name = ctx.profileUrlToName.get(pUrl);
776
- if (name && name !== ctx.profileName) {
777
- delegateInterfaceName = name;
778
- ctx.validatorImports.set(name, `./${name}.js`);
779
- break;
780
- }
781
- }
782
- }
783
- // When delegating, annotate the array so .resource is accessible without casts
784
- const entriesType = '';
785
- let code = `
786
- // Required slice validation for ${relPath}:${sliceLabel} (profile discriminator → ${resourceType})
787
- {
788
- const _${varName}Entries${entriesType} = elements(${arrayAccessor}).filter(
789
- (e) => at(e, 'resource', 'resourceType') === '${resourceType}'
790
- );`;
791
- if (min > 0) {
792
- code += `
793
- if (_${varName}Entries.length < ${min}) {
794
- errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
795
- }`;
796
- }
797
- if (max !== undefined) {
798
- code += `
799
- if (_${varName}Entries.length > ${max}) {
800
- errors.push("${errorPath}:${sliceLabel}: max allowed = ${max}, but found " + _${varName}Entries.length);
801
- }`;
802
- }
803
- // Delegate to profiled validator for each matched entry
804
- if (delegateInterfaceName) {
805
- code += `
806
- for (const _e of _${varName}Entries) {
807
- if (at(_e, 'resource')) {
808
- const _sub = await validate${delegateInterfaceName}(asResource(at(_e, 'resource')), options);
809
- errors.push(..._sub.errors);
810
- warnings.push(..._sub.warnings);
811
- }
812
- }`;
813
- }
814
- code += `
815
- }`;
816
- out.push(code);
632
+ function isUrlDiscriminatedExtensionSlice(slice, baseName, fields) {
633
+ // The body tests hasPatternCodeableConcept and hasPatternCoding *before* any
634
+ // extension branch, so a slice carrying a pattern reaches a pattern branch and
635
+ // would emit pattern checks under a minimum of zero. Two regressions came from
636
+ // admitting those.
637
+ if (slice.patternConstraint !== undefined)
638
+ return false;
639
+ if (slice.binding?.codes?.length)
640
+ return false;
641
+ // The body detector, not a hand-rolled copy. My first version required a pinned
642
+ // `url` child, which DTRStdQuestionnaire's item.extension:contextExpression does
643
+ // not have — it names its canonical through type.profile, so detectExtensionUrlSlice
644
+ // reads it from profileUrls. The predicate said no, the slice bailed, and nothing
645
+ // about a present instance was checked.
646
+ const relPath = baseName.replace(/^[^.]+\./, '');
647
+ return detectExtensionUrlSlice(slice, relPath, fields) !== undefined;
817
648
  }