babelfhir-ts 1.5.16 → 1.5.18

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 (23) hide show
  1. package/out/src/generator/core/generatedPackageDeps.js +78 -0
  2. package/out/src/generator/emitters/interface/processNestedField.js +14 -8
  3. package/out/src/generator/emitters/prefab/prefabEmitter.js +7 -3
  4. package/out/src/generator/emitters/prefab/prefabRenderer.js +13 -6
  5. package/out/src/generator/emitters/prefab/prefabRuntimeAssets.js +1 -1
  6. package/out/src/generator/emitters/validator/fhirpathStubInstaller.js +6 -1
  7. package/out/src/generator/emitters/validator/sliceBackboneValidation.js +11 -13
  8. package/out/src/generator/emitters/validator/sliceExtensionValidation.js +22 -19
  9. package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +6 -6
  10. package/out/src/generator/emitters/validator/sliceValidatorUtils.js +23 -8
  11. package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +29 -31
  12. package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +1 -1
  13. package/out/src/generator/emitters/validator/validatorFieldBuilders.js +49 -51
  14. package/out/src/generator/emitters/validator/validatorGenerator.js +56 -5
  15. package/out/src/generator/emitters/valueset/valueSetGenerator.js +45 -26
  16. package/out/src/generator/fhir/versionContext.js +14 -0
  17. package/out/src/generator/fhir/versionContext.ts +15 -0
  18. package/out/src/generator/generationHelpers.js +12 -26
  19. package/out/src/generator/index.js +10 -51
  20. package/out/src/generator/sdProcessor.js +1 -1
  21. package/out/src/main.js +2 -2
  22. package/package.json +29 -22
  23. package/parity-matrix.json +186 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Dependency ranges written into a generated package's `package.json`.
3
+ *
4
+ * Single source of truth: the generated-package manifest is assembled in two
5
+ * places (the archive pipeline and the directory pipeline in
6
+ * `src/generator/index.ts`), and both must emit identical ranges. Keep the
7
+ * ranges here, not inline at the call sites.
8
+ *
9
+ * These ranges describe what the *consumer* of a generated package installs,
10
+ * so they are deliberately wider than this repo's own devDependencies.
11
+ */
12
+ /**
13
+ * Runtime/peer ranges emitted into generated packages.
14
+ *
15
+ * `fhirpath` is a peer so the host decides the version. All of 3.x/4.x/5.x are
16
+ * accepted: generated validators only call `fhirpath.evaluate()` and import
17
+ * `fhirpath/fhir-context/<model>/index.js`, both of which are stable across
18
+ * those majors. The `/index.js` filename is required — fhirpath >= 5 declares
19
+ * an `exports` map, so extensionless deep paths no longer resolve.
20
+ */
21
+ export const EMITTED_RANGES = {
22
+ /** FHIR type declarations backing the emitted `.d.ts` files. */
23
+ fhirTypes: '^0.0.41',
24
+ /** FHIRPath engine used by emitted constraint validators. */
25
+ fhirpath: '^3.0.0 || ^4.0.0 || ^5.0.0',
26
+ /** Schema runtime for `--schema zod` output. */
27
+ zod: '^4.0.0',
28
+ /**
29
+ * Prefab UI runtime for `--prefab` output.
30
+ *
31
+ * 0.3.0 is the floor: it introduced wire protocol 0.3, where the theme is
32
+ * compiled into the top-level `css` array, `stylesheets` carries external
33
+ * URLs, and `mode` forces a colour scheme. Emitted renderers pass `theme`
34
+ * and spread `PrefabAppOptions` into `PrefabApp`, so they rely on a >= 0.3
35
+ * serializer/renderer pair to apply themes. A 0.2.x renderer would silently
36
+ * drop them.
37
+ */
38
+ prefab: '^0.3.0',
39
+ /** Generated FHIR client base package (`@babelfhir-ts/client-<slug>`). */
40
+ client: '^0.2.0',
41
+ /** Shared base zod schemas (`@babelfhir-ts/zod`). */
42
+ zodBase: '^0.2.0',
43
+ /** DICOMweb helpers (`@babelfhir-ts/dicomweb`). */
44
+ dicomweb: '^0.1.0',
45
+ };
46
+ /**
47
+ * Build the dependency sections of a generated package's `package.json` from
48
+ * the active generation flags.
49
+ *
50
+ * @param flags Generation flags for this run.
51
+ * @param clientSlug FHIR version slug used in the client package name.
52
+ */
53
+ export function buildGeneratedPackageDeps(flags, clientSlug) {
54
+ // Type definitions for FHIR are needed for the emitted .d.ts files.
55
+ const dependencies = {
56
+ '@types/fhir': EMITTED_RANGES.fhirTypes,
57
+ };
58
+ // fhirpath is required at runtime by validators; keep as peer so the host
59
+ // decides the version.
60
+ const peerDependencies = {
61
+ fhirpath: EMITTED_RANGES.fhirpath,
62
+ };
63
+ // The generated client extends the versioned base client package.
64
+ if (!flags?.noClient) {
65
+ dependencies[`@babelfhir-ts/client-${clientSlug}`] = EMITTED_RANGES.client;
66
+ }
67
+ if (flags?.schema === 'zod') {
68
+ dependencies['@babelfhir-ts/zod'] = EMITTED_RANGES.zodBase;
69
+ peerDependencies.zod = EMITTED_RANGES.zod;
70
+ }
71
+ if (flags?.dicomweb) {
72
+ dependencies['@babelfhir-ts/dicomweb'] = EMITTED_RANGES.dicomweb;
73
+ }
74
+ if (flags?.prefab) {
75
+ dependencies['@maxhealth.tech/prefab'] = EMITTED_RANGES.prefab;
76
+ }
77
+ return { dependencies, peerDependencies };
78
+ }
@@ -29,14 +29,6 @@ export function handleNestedField(args) {
29
29
  const fieldPathParts = field.name.split('.');
30
30
  const isExtensionField = fieldPathParts[fieldPathParts.length - 1] === 'extension' ||
31
31
  (field.sliceName && fieldPathParts[fieldPathParts.length - 2] === 'extension');
32
- if (field.name.includes('bodySite')) {
33
- logger.debug('[DEBUG bodySite treatAsNested]', {
34
- fieldName: field.name,
35
- sliceName: field.sliceName,
36
- isExtensionField,
37
- treatAsNested
38
- });
39
- }
40
32
  if (isExtensionField) {
41
33
  const hasExtensionSlices = fields.some(f => (f.name === field.name || f.name === `${field.name}.extension`) &&
42
34
  f.sliceName &&
@@ -46,6 +38,20 @@ export function handleNestedField(args) {
46
38
  treatAsNested = false;
47
39
  }
48
40
  }
41
+ // TODO(bug): `treatAsNested` is computed but never consulted when deciding
42
+ // whether to emit the nested interface below — both `= false` guards (payor
43
+ // without an explicit profile, and extension fields carrying slices) are
44
+ // therefore dead, contrary to their comments above. Honouring it would change
45
+ // emitted interfaces for those fields, so it needs a parity run rather than a
46
+ // drive-by fix. The debug log below is currently its only reader.
47
+ if (field.name.includes('bodySite')) {
48
+ logger.debug('[DEBUG bodySite treatAsNested]', {
49
+ fieldName: field.name,
50
+ sliceName: field.sliceName,
51
+ isExtensionField,
52
+ treatAsNested
53
+ });
54
+ }
49
55
  const nestedInterfaceName = `${parentInterfaceName}${capitalize(fieldName)}`;
50
56
  const nestedFields = fields.filter((f) => f.name.startsWith(`${field.name}.`));
51
57
  // Debug answer nested interface issue
@@ -124,9 +124,13 @@ function buildMcpViewsTs(profiles) {
124
124
  const detailEntries = [];
125
125
  for (const [resourceType, interfaceName] of byResourceType) {
126
126
  const varBase = lowerFirst(interfaceName);
127
- imports.push(`import { ${varBase}Table, ${varBase}Detail } from './${interfaceName}Prefab.js';`);
128
- tableEntries.push(` ${JSON.stringify(resourceType)}: (resources) => ${varBase}Table(resources as any),`);
129
- detailEntries.push(` ${JSON.stringify(resourceType)}: (resource) => ${varBase}Detail(resource as any),`);
127
+ imports.push(`import { ${varBase}Table, ${varBase}Detail } from './${interfaceName}Prefab.js';`, `import type { ${interfaceName} } from '../${interfaceName}.js';`);
128
+ // Narrow the registry's `unknown` payload to the profile interface rather
129
+ // than widening to `any`: the entry has already been selected by
130
+ // `resourceType`, and asserting to the concrete type keeps every downstream
131
+ // property access type-checked inside the view function.
132
+ tableEntries.push(` ${JSON.stringify(resourceType)}: (resources) => ${varBase}Table(resources as ${interfaceName}[]),`);
133
+ detailEntries.push(` ${JSON.stringify(resourceType)}: (resource) => ${varBase}Detail(resource as ${interfaceName}),`);
130
134
  }
131
135
  return `/**
132
136
  * AUTO-GENERATED by babelfhir-ts --prefab — do not edit.
@@ -248,7 +248,7 @@ export function generatePrefabRenderer(interfaceName, fields, options) {
248
248
  ? `rowRecord.${titleDescriptor.name} ?? '${interfaceName}'`
249
249
  : `'${interfaceName}'`;
250
250
  // --- Browser (interactive master/detail) variant -----------------------
251
- // Uses prefab 0.1.9 reactive primitives: `collection`, `signal`, `Ref`,
251
+ // Uses prefab reactive primitives: `collection`, `signal`, `Ref`,
252
252
  // `MasterDetail`, `Detail`, and the `find` pipe to resolve the selected
253
253
  // row. Columns and detail rows consume FHIR datatype pipes registered by
254
254
  // `./registerFhirPipes.js` (`humanName`, `contactPointBySystem`, etc.).
@@ -375,7 +375,7 @@ ${prefabImportLines}
375
375
  } from '@maxhealth.tech/prefab';
376
376
  ${prefabTypeImportLine}
377
377
  import type { ${interfaceName} } from '../${interfaceName}.js';
378
- ${formatterImportBlock}import { styles } from './styles.js';
378
+ ${formatterImportBlock}import { styles, theme } from './styles.js';
379
379
  // Self-contained FHIR datatype pipes. Passed through \`PrefabApp({ pipes })\`
380
380
  // so the renderer can serialize them into the wire format and re-hydrate
381
381
  // with \`new Function()\` in the browser — closures are not preserved, so
@@ -527,7 +527,9 @@ ${browserDetailRowsLiteral}
527
527
  * own outer \`PrefabApp\`.
528
528
  *
529
529
  * Prerequisites (auto-imported at the top of this module):
530
- * - \`@maxhealth.tech/prefab\` >= 0.1.9 (reactive primitives)
530
+ * - \`@maxhealth.tech/prefab\` >= 0.3.0 (wire protocol 0.3: the theme is
531
+ * compiled into the top-level \`css\` array, \`stylesheets\` carries
532
+ * external URLs, and \`mode\` forces a colour scheme)
531
533
  * - \`./registerFhirPipes.js\` (FHIR datatype pipes: humanName, …)
532
534
  */
533
535
  export function ${varBase}Browser(
@@ -535,18 +537,23 @@ export function ${varBase}Browser(
535
537
  options?: Partial<Omit<PrefabAppOptions, 'view'>> & { masterWidth?: string },
536
538
  ) {
537
539
  // Destructure the Browser-only \`masterWidth\` away from \`PrefabAppOptions\`
538
- // passthrough so remaining keys (theme, scripts, onMount, …) spread
540
+ // passthrough so remaining keys (css, mode, scripts, onMount, …) spread
539
541
  // cleanly into the PrefabApp constructor.
540
- const { masterWidth, title, pipes: userPipes, ...appOptions } = options ?? {};
542
+ const { masterWidth, title, pipes: userPipes, theme: userTheme, ...appOptions } = options ?? {};
541
543
 
542
544
  const { view } = ${varBase}BrowserView(resources, { masterWidth });
543
545
 
544
546
  return new PrefabApp({
545
547
  ...appOptions,
546
548
  title: title ?? '${interfaceName} Browser',
549
+ // The theme from \`prefab/styles.ts\` is applied by default so editing that
550
+ // file (or passing \`--prefab-styles\`) actually takes effect. prefab >= 0.3
551
+ // compiles it into the wire \`css\` array; an empty theme contributes
552
+ // nothing. A caller-supplied \`theme\` replaces it wholesale.
553
+ theme: userTheme ?? theme,
547
554
  // FHIR datatype pipes ship inline with the wire format so the browser
548
555
  // renderer hydrates them at mount time. User-supplied \`pipes\` override
549
- // individual entries by name (same shallow-merge semantics as theme).
556
+ // individual entries by name.
550
557
  pipes: { ...FHIR_PIPES, ...(userPipes ?? {}) },
551
558
  view,
552
559
  });
@@ -170,7 +170,7 @@ export type PrefabTheme = typeof theme;
170
170
  * of self-contained (closure-free) pipe functions plus a `registerFhirPipes`
171
171
  * convenience for Node-side registration.
172
172
  *
173
- * **Why self-contained?** prefab @0.2.1 serializes pipes via `fn.toString()`
173
+ * **Why self-contained?** prefab serializes pipes via `fn.toString()`
174
174
  * and hydrates them in the browser with `new Function(source)` — closures are
175
175
  * not preserved. Every helper in this module therefore inlines the logic it
176
176
  * needs (no cross-function calls, no imports from `./fhirFormat.js`). The
@@ -9,7 +9,12 @@
9
9
  */
10
10
  import fs from 'fs';
11
11
  import path from 'path';
12
- const FHIR_CONTEXT_VERSIONS = ['r4', 'r4b', 'r5'];
12
+ // Mirrors the `fhir-context/` models the real fhirpath package ships (dstu2,
13
+ // stu3, r4, r5 — there is no r4b model). Only the two we can emit are stubbed:
14
+ // stubbing a model fhirpath does not ship would let a bad import specifier pass
15
+ // `tsc` here and then fail at runtime in the consumer's install.
16
+ // Keep in sync with fhirpathModelSlug() in src/generator/fhir/versionContext.ts.
17
+ const FHIR_CONTEXT_VERSIONS = ['r4', 'r5'];
13
18
  export function installFhirpathStub(outputDir) {
14
19
  const pkgDir = path.join(outputDir, 'node_modules', 'fhirpath');
15
20
  fs.mkdirSync(pkgDir, { recursive: true });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * BackboneElement and binding-codes slice validation strategies.
3
3
  */
4
- import { safeAccessor, sliceElseBranch, detectNestedArray, FHIR_PRIMITIVE_STRING_TYPES, collectWithinSliceMaxChildren, generateWithinSliceMaxChecks } from './sliceValidatorUtils.js';
4
+ import { safeAccessor, safeList, sliceElseBranch, detectNestedArray, FHIR_PRIMITIVE_STRING_TYPES, collectWithinSliceMaxChildren, generateWithinSliceMaxChecks } from './sliceValidatorUtils.js';
5
5
  /** Collect required direct children of a BackboneElement slice for per-element validation. */
6
6
  export function collectRequiredSliceChildren(sliceElementId, fields, discriminatorProp, profileName, relPath) {
7
7
  const result = [];
@@ -199,7 +199,6 @@ export function generateBackboneElementValidation(slice, relPath, errorPath, sli
199
199
  }).join('');
200
200
  const maxChildren = collectWithinSliceMaxChildren(slice, fields);
201
201
  const maxChecksCode = generateWithinSliceMaxChecks(maxChildren, `${varName}Elements`, resourceType, relPath, sliceLabel);
202
- const hasAnyChildChecks = requiredChildren.length > 0 || nestedChildren.length > 0 || maxChildren.length > 0;
203
202
  const childChecksCode = (requiredChildren.length > 0 || nestedChildren.length > 0
204
203
  ? `\n for (const _matched of ${varName}Elements) {
205
204
  const _m = _matched as any;${directChecksInner}${nestedChecksInner}
@@ -240,8 +239,8 @@ export function generateBackboneElementValidation(slice, relPath, errorPath, sli
240
239
  const bbeElseBranch = sliceElseBranch(relPath, errorPath, sliceLabel);
241
240
  out.push(`
242
241
  // Required BackboneElement slice validation for ${relPath}:${sliceLabel} (discriminated by ${discriminatorProp})
243
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)})) {
244
- const ${varName}Elements = ${safeAccessor(relPath)}.filter((item: any) => ${checks});
242
+ if (Array.isArray(${safeAccessor(relPath)})) {
243
+ const ${varName}Elements = ${safeList(relPath)}.filter((item: any) => ${checks});
245
244
  if (${varName}Elements.length < ${min}) {
246
245
  errors.push("No elements matched required slice: '${errorPath}:${sliceLabel}'");
247
246
  }${childChecksCode}
@@ -309,7 +308,6 @@ export function generateFixedValueSliceValidation(slice, relPath, errorPath, sli
309
308
  }).join('');
310
309
  const maxChildren = collectWithinSliceMaxChildren(slice, fields);
311
310
  const maxChecksCode = generateWithinSliceMaxChecks(maxChildren, `${varName}Elements`, resourceType, relPath, sliceLabel);
312
- const hasChildChecks = requiredChildren.length > 0 || nestedChildren.length > 0 || fixedChildren.length > 0 || maxChildren.length > 0;
313
311
  const childChecksCode = (requiredChildren.length > 0 || nestedChildren.length > 0 || fixedChildren.length > 0
314
312
  ? `\n for (const _matched of ${varName}Elements) {
315
313
  const _m = _matched as any;${directChecksInner}${nestedChecksInner}${fixedChecksInner}
@@ -374,8 +372,8 @@ export function generateFixedValueSliceValidation(slice, relPath, errorPath, sli
374
372
  const bbeElseBranch = sliceElseBranch(relPath, errorPath, sliceLabel);
375
373
  out.push(`
376
374
  // Required fixedValue slice validation for ${relPath}:${sliceLabel} (discriminated by ${discriminatorProp} === ${fixedVal})
377
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)})) {
378
- const ${varName}Elements = ${safeAccessor(relPath)}.filter((item: any) => ${checks});
375
+ if (Array.isArray(${safeAccessor(relPath)})) {
376
+ const ${varName}Elements = ${safeList(relPath)}.filter((item: any) => ${checks});
379
377
  if (${varName}Elements.length < ${min}) {
380
378
  errors.push("No elements matched required slice: '${errorPath}:${sliceLabel}'");
381
379
  }${childChecksCode}
@@ -424,8 +422,8 @@ export function generateBindingCodesValidation(slice, baseName, relPath, errorPa
424
422
  if (slice.isArray) {
425
423
  out.push(`
426
424
  // Slice cardinality check for ${baseName}:${sliceLabel} (primitive code array)
427
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)}) && ${safeAccessor(relPath)}.length > 0) {
428
- const ${varName}Count = ${safeAccessor(relPath)}.filter(item =>
425
+ if (${safeList(relPath)}.length > 0) {
426
+ const ${varName}Count = ${safeList(relPath)}.filter(item =>
429
427
  [${codes}].includes(item as string)
430
428
  ).length;
431
429
  if (${varName}Count < ${min}) {
@@ -452,8 +450,8 @@ export function generateBindingCodesValidation(slice, baseName, relPath, errorPa
452
450
  if (isCodingType) {
453
451
  out.push(`
454
452
  // Slice cardinality check for ${baseName}:${sliceLabel} (Coding array)
455
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)}) && ${safeAccessor(relPath)}.length > 0) {
456
- const ${varName}Count = ${safeAccessor(relPath)}.filter((item: any) =>
453
+ if (${safeList(relPath)}.length > 0) {
454
+ const ${varName}Count = ${safeList(relPath)}.filter((item: any) =>
457
455
  [${codes}].includes(item.code as string)
458
456
  ).length;
459
457
  if (${varName}Count < ${min}) {
@@ -464,8 +462,8 @@ export function generateBindingCodesValidation(slice, baseName, relPath, errorPa
464
462
  else {
465
463
  out.push(`
466
464
  // Slice cardinality check for ${baseName}:${sliceLabel}
467
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)}) && ${safeAccessor(relPath)}.length > 0) {
468
- const ${varName}Count = ${safeAccessor(relPath)}.filter((item: any) =>
465
+ if (${safeList(relPath)}.length > 0) {
466
+ const ${varName}Count = ${safeList(relPath)}.filter((item: any) =>
469
467
  (item.coding as Array<{ code?: string }> | undefined)?.some(coding => [${codes}].includes(coding.code as string))
470
468
  ).length;
471
469
  if (${varName}Count < ${min}) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Extension and ValueSet slice validation strategies.
3
3
  */
4
- import { safeAccessor, sliceElseBranch, detectNestedArray, findArrayAncestor, isArrayPath, FHIR_PRIMITIVE_STRING_TYPES } from './sliceValidatorUtils.js';
4
+ import { safeAccessor, safeList, sliceElseBranch, detectNestedArray, findArrayAncestor, isArrayPath, FHIR_PRIMITIVE_STRING_TYPES } from './sliceValidatorUtils.js';
5
5
  export function generateValueSetValidation(slice, baseName, relPath, errorPath, sliceLabel, varName, min, minText, getValueSetForBinding, valueSetImports, fields, out) {
6
6
  const vsInfo = getValueSetForBinding(slice.binding.uri);
7
7
  const vsInfoFull = vsInfo && !('systemOnly' in vsInfo && vsInfo.systemOnly) ? vsInfo : undefined;
@@ -45,8 +45,8 @@ export function generateValueSetValidation(slice, baseName, relPath, errorPath,
45
45
  if (isPrimitiveString) {
46
46
  out.push(`
47
47
  // Slice validation for ${baseName}:${sliceLabel} using ValueSet validator (primitive string array)
48
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)}) && ${safeAccessor(relPath)}.length > 0) {
49
- const ${varName}Count = ${safeAccessor(relPath)}.filter((item: any) =>
48
+ if (${safeList(relPath)}.length > 0) {
49
+ const ${varName}Count = ${safeList(relPath)}.filter((item: any) =>
50
50
  typeof item === 'string' && ${vsInfoFull.validatorFn}(item)
51
51
  ).length;
52
52
  if (${varName}Count < ${min}) {
@@ -57,8 +57,8 @@ export function generateValueSetValidation(slice, baseName, relPath, errorPath,
57
57
  else if (isCodingType) {
58
58
  out.push(`
59
59
  // Slice validation for ${baseName}:${sliceLabel} using ValueSet validator (Coding array)
60
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)}) && ${safeAccessor(relPath)}.length > 0) {
61
- const ${varName}Count = ${safeAccessor(relPath)}.filter((item: any) =>
60
+ if (${safeList(relPath)}.length > 0) {
61
+ const ${varName}Count = ${safeList(relPath)}.filter((item: any) =>
62
62
  item.code && ${vsInfoFull.validatorFn}(item.code)
63
63
  ).length;
64
64
  if (${varName}Count < ${min}) {
@@ -69,8 +69,8 @@ export function generateValueSetValidation(slice, baseName, relPath, errorPath,
69
69
  else {
70
70
  out.push(`
71
71
  // Slice validation for ${baseName}:${sliceLabel} using ValueSet validator
72
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)}) && ${safeAccessor(relPath)}.length > 0) {
73
- const ${varName}Count = ${safeAccessor(relPath)}.filter((item: any) =>
72
+ if (${safeList(relPath)}.length > 0) {
73
+ const ${varName}Count = ${safeList(relPath)}.filter((item: any) =>
74
74
  (item.coding as Array<{ code?: string }> | undefined)?.some(coding => coding.code && ${vsInfoFull.validatorFn}(coding.code))
75
75
  ).length;
76
76
  if (${varName}Count < ${min}) {
@@ -147,8 +147,8 @@ export function generateValueSetValidation(slice, baseName, relPath, errorPath,
147
147
  : `(item.coding as Array<{ system?: string }> | undefined)?.some(coding => coding.system !== undefined && ${systemsArr}.includes(coding.system))`;
148
148
  out.push(`
149
149
  // Slice system-only validation for ${baseName}:${sliceLabel} (ValueSet '${vsName}')
150
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)}) && ${safeAccessor(relPath)}.length > 0) {
151
- const ${varName}Count = ${safeAccessor(relPath)}.filter((item: any) =>
150
+ if (${safeList(relPath)}.length > 0) {
151
+ const ${varName}Count = ${safeList(relPath)}.filter((item: any) =>
152
152
  ${systemCheck}
153
153
  ).length;
154
154
  if (${varName}Count < ${min}) {
@@ -234,8 +234,8 @@ export function generateExistsExtensionValidation(slice, relPath, errorPath, sli
234
234
  if (slice.isArray || baseFieldIsArray) {
235
235
  out.push(`
236
236
  // Required slice validation for ${relPath}:${sliceLabel} (exists extension discriminator)
237
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)})) {
238
- const ${varName}Elements = ${safeAccessor(relPath)}.filter((item: any) =>
237
+ if (Array.isArray(${safeAccessor(relPath)})) {
238
+ const ${varName}Elements = ${safeList(relPath)}.filter((item: any) =>
239
239
  item.extension?.some((ext: { url?: string }) => ext.url === "${extensionUrl}")
240
240
  );
241
241
  if (${varName}Elements.length < ${min}) {
@@ -322,8 +322,8 @@ export function generateExtensionUrlSliceValidation(slice, relPath, errorPath, s
322
322
  const elseGuard = parentPath ? `else if (${safeAccessor(parentPath)})` : 'else';
323
323
  out.push(`
324
324
  // Required extension slice validation for ${relPath}:${sliceLabel} (url discriminator)
325
- if (${parentGuard}${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)})) {
326
- const ${varName}Elements = ${safeAccessor(relPath)}.filter((item: any) =>
325
+ if (${parentGuard}Array.isArray(${safeAccessor(relPath)})) {
326
+ const ${varName}Elements = ${safeList(relPath)}.filter((item: any) =>
327
327
  item.url === "${extensionUrl}"
328
328
  );
329
329
  if (${varName}Elements.length < ${min}) {
@@ -367,7 +367,7 @@ function generateExtensionValueChecks(matchedArrayVar, extensionUrl, extMeta, ct
367
367
  lines.push(`${indent}for (const _ext of ${matchedArrayVar}) {`);
368
368
  // -- Value type check --
369
369
  // Use Object.entries to get both key and value without re-indexing into the typed object
370
- lines.push(`${indent} const _valEntry = Object.entries(_ext).find(([k]) => k.startsWith("value"));`);
370
+ lines.push(`${indent} const _valEntry = Object.entries(node(_ext)).find(([k]) => k.startsWith("value"));`);
371
371
  lines.push(`${indent} if (_valEntry && !${allowedSet}.includes(_valEntry[0])) {`);
372
372
  lines.push(`${indent} const _found = _valEntry[0].replace("value", "").toLowerCase();`);
373
373
  lines.push(`${indent} errors.push("The Profile '${profileOrExtUrl}' definition allows for ${typesClause} but found type " + _found);`);
@@ -386,22 +386,25 @@ function generateExtensionValueChecks(matchedArrayVar, extensionUrl, extMeta, ct
386
386
  // when valueCode was expected). This mirrors the HL7 validator which
387
387
  // checks the value against the binding regardless of the value type.
388
388
  if (extMeta.valueTypes.length === 1 && (extMeta.valueTypes[0] === 'code' || extMeta.valueTypes[0] === 'string' || extMeta.valueTypes[0] === 'uri')) {
389
- const propName = `value${valuePropertySuffix(extMeta.valueTypes[0])}`;
390
389
  lines.push(`${indent} if (_valEntry && typeof _valEntry[1] === "string" && !${vsInfoFull.validatorFn}(_valEntry[1])) {`);
391
390
  lines.push(`${indent} errors.push("The value provided ('" + _valEntry[1] + "') was not found in the value set '${vsInfoFull.displayName}' (${vsUri})");`);
392
391
  lines.push(`${indent} }`);
393
392
  }
394
393
  else if (extMeta.valueTypes.includes('CodeableConcept')) {
395
- lines.push(`${indent} if (_ext.valueCodeableConcept?.coding) {`);
396
- lines.push(`${indent} const _hasValid = _ext.valueCodeableConcept.coding.some((c: { code?: string }) => c.code && ${vsInfoFull.validatorFn}(c.code));`);
394
+ // `_ext` is an element of an `arr()` list and therefore `unknown`; reach the
395
+ // coding through `at()` and assert only the shape actually read.
396
+ lines.push(`${indent} const _extCoding = at(_ext, 'valueCodeableConcept', 'coding') as Array<{ code?: string }> | undefined;`);
397
+ lines.push(`${indent} if (_extCoding) {`);
398
+ lines.push(`${indent} const _hasValid = _extCoding.some(c => c.code && ${vsInfoFull.validatorFn}(c.code));`);
397
399
  lines.push(`${indent} if (!_hasValid) {`);
398
400
  lines.push(`${indent} errors.push("None of the codings in the extension value are from the value set '${vsInfoFull.displayName}' (${vsUri})");`);
399
401
  lines.push(`${indent} }`);
400
402
  lines.push(`${indent} }`);
401
403
  }
402
404
  else if (extMeta.valueTypes.includes('Coding')) {
403
- lines.push(`${indent} if (_ext.valueCoding?.code && !${vsInfoFull.validatorFn}(_ext.valueCoding.code)) {`);
404
- lines.push(`${indent} errors.push("The value provided ('" + _ext.valueCoding.code + "') was not found in the value set '${vsInfoFull.displayName}' (${vsUri})");`);
405
+ lines.push(`${indent} const _extCode = at(_ext, 'valueCoding', 'code') as string | undefined;`);
406
+ lines.push(`${indent} if (_extCode && !${vsInfoFull.validatorFn}(_extCode)) {`);
407
+ lines.push(`${indent} errors.push("The value provided ('" + _extCode + "') was not found in the value set '${vsInfoFull.displayName}' (${vsUri})");`);
405
408
  lines.push(`${indent} }`);
406
409
  }
407
410
  }
@@ -4,7 +4,7 @@
4
4
  * (patternCodeableConcept, patternCoding, BackboneElement, binding, ValueSet).
5
5
  */
6
6
  import { addDiagnostic } from '../../core/sdDiagnostics.js';
7
- import { safeAccessor, sliceElseBranch, rewritePrimitiveExtensionPath, isArrayPath, collectWithinSliceMaxChildren, generateWithinSliceMaxChecks } from './sliceValidatorUtils.js';
7
+ import { safeAccessor, safeList, sliceElseBranch, rewritePrimitiveExtensionPath, isArrayPath, collectWithinSliceMaxChildren, generateWithinSliceMaxChecks } from './sliceValidatorUtils.js';
8
8
  import { generateBackboneElementValidation, generateBindingCodesValidation, generateFixedValueSliceValidation } from './sliceBackboneValidation.js';
9
9
  import { generateValueSetValidation, detectExistsExtensionDiscriminator, generateExistsExtensionValidation, detectExtensionUrlSlice, generateExtensionUrlSliceValidation, } from './sliceExtensionValidation.js';
10
10
  /**
@@ -235,7 +235,7 @@ function generateCrossSliceExclusivityValidations(slicesByBase, fields, fieldPat
235
235
  if (!isArrayPath(relPath, fieldPathMap, arrayFieldPaths))
236
236
  return;
237
237
  let code = `\n // Cross-slice exclusivity validation for ${relPath}`;
238
- code += `\n if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)})) {`;
238
+ code += `\n if (Array.isArray(${safeAccessor(relPath)})) {`;
239
239
  code += `\n for (const _el of ${safeAccessor(relPath)}) {`;
240
240
  code += `\n const _matchedSlices: string[] = [];`;
241
241
  for (const m of matchers) {
@@ -374,8 +374,8 @@ function generatePatternCodeableConceptValidation(slice, relPath, errorPath, sli
374
374
  const elseBranch = sliceElseBranch(relPath, errorPath, sliceLabel);
375
375
  out.push(`
376
376
  // Required slice validation for ${relPath}:${sliceLabel}
377
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)})) {
378
- const ${varName}Elements = ${safeAccessor(relPath)}.filter((item: any) => ${checks});
377
+ if (Array.isArray(${safeAccessor(relPath)})) {
378
+ const ${varName}Elements = ${safeList(relPath)}.filter((item: any) => ${checks});
379
379
  if (${varName}Elements.length < ${min}) {
380
380
  errors.push("No elements matched required slice: '${errorPath}:${sliceLabel}'");
381
381
  }${childMaxChecks}
@@ -698,8 +698,8 @@ function generateReferenceProfileDiscriminatorValidation(slice, relPath, errorPa
698
698
  const childMaxChecks = generateWithinSliceMaxChecks(maxChildren, `_${varName}Refs`, resType, relPath, sliceLabel);
699
699
  out.push(`
700
700
  // Required Reference-profile slice validation for ${relPath}:${sliceLabel} (target → ${targetResourceType})
701
- if (${safeAccessor(relPath)} && Array.isArray(${safeAccessor(relPath)})) {
702
- const _${varName}Refs = ${safeAccessor(relPath)}.filter((e: any) =>
701
+ if (Array.isArray(${safeAccessor(relPath)})) {
702
+ const _${varName}Refs = ${safeList(relPath)}.filter((e: any) =>
703
703
  e?.type === '${targetResourceType}' ||
704
704
  (typeof e?.reference === 'string' && (e.reference.startsWith('${targetResourceType}/') || new RegExp('/${targetResourceType}/').test(e.reference)))
705
705
  );
@@ -12,17 +12,32 @@ export const FHIR_PRIMITIVES = new Set([
12
12
  export const FHIR_PRIMITIVE_STRING_TYPES = new Set([
13
13
  'code', 'uri', 'url', 'string', 'id', 'oid', 'uuid', 'canonical',
14
14
  ]);
15
+ /** Render a dotted path as `at()` arguments: `'channel', 'payload', 'extension'`. */
16
+ export function atArgs(relPath) {
17
+ return relPath.split('.').map(p => `'${p}'`).join(', ');
18
+ }
15
19
  /**
16
20
  * Convert a dotted relPath into a safe accessor expression for generated code.
17
21
  * For single-segment paths (e.g., "category"), returns `resource.category`.
18
22
  * For multi-segment paths (e.g., "channel.payload.extension"), returns
19
- * `(resource as any).channel?.payload?.extension` to avoid TS2339 errors.
23
+ * `at(resource, 'channel', 'payload', 'extension')` — typed as `unknown`, so
24
+ * the emitted code stays checked instead of being widened by an `as any` cast.
20
25
  */
21
26
  export function safeAccessor(relPath) {
22
27
  if (!relPath.includes('.'))
23
28
  return `resource.${relPath}`;
24
- const parts = relPath.split('.');
25
- return `(resource as any).${parts[0]}?.${parts.slice(1).join('?.')}`;
29
+ return `at(resource, ${atArgs(relPath)})`;
30
+ }
31
+ /**
32
+ * Like {@link safeAccessor} but typed as a list, for call sites that iterate,
33
+ * `.filter()` or read `.length`.
34
+ *
35
+ * `arr()` yields `[]` for absent/non-array values, so
36
+ * `safeList(p) + '.length > 0'` is equivalent to the
37
+ * `x && Array.isArray(x) && x.length > 0` guard it replaces.
38
+ */
39
+ export function safeList(relPath) {
40
+ return `arr(${safeAccessor(relPath)})`;
26
41
  }
27
42
  /**
28
43
  * Generate an else branch for required slice validation that guards on parent existence.
@@ -31,8 +46,8 @@ export function sliceElseBranch(relPath, errorPath, sliceLabel) {
31
46
  if (!relPath.includes('.')) {
32
47
  return `} else {\n errors.push("No elements matched required slice: '${errorPath}:${sliceLabel}'");\n }`;
33
48
  }
34
- const parentPath = relPath.split('.').slice(0, -1).join('?.');
35
- return `} else if ((resource as any).${parentPath}) {\n errors.push("No elements matched required slice: '${errorPath}:${sliceLabel}'");\n }`;
49
+ const parentPath = relPath.split('.').slice(0, -1).join('.');
50
+ return `} else if (at(resource, ${atArgs(parentPath)})) {\n errors.push("No elements matched required slice: '${errorPath}:${sliceLabel}'");\n }`;
36
51
  }
37
52
  /** Detect whether a dotted relPath traverses an array-typed parent field. */
38
53
  export function detectNestedArray(relPath, fields) {
@@ -148,7 +163,7 @@ export function generateWithinSliceMaxChecks(children, elementsVarName, resType,
148
163
  // max=0 on choice type (e.g., value[x]): check no key starting with prefix exists
149
164
  code += `
150
165
  for (const _wsElem of ${elementsVarName}) {
151
- if (Object.keys(_wsElem).some(k => k.startsWith("${childProp}"))) {
166
+ if (Object.keys(node(_wsElem)).some(k => k.startsWith("${childProp}"))) {
152
167
  errors.push("${resType}.${relPath}:${sliceLabel}.${fhirPath}: max allowed = 0, but found 1");
153
168
  }
154
169
  }`;
@@ -157,7 +172,7 @@ export function generateWithinSliceMaxChecks(children, elementsVarName, resType,
157
172
  // max=0: field must not exist at all — handles both scalar and array
158
173
  code += `
159
174
  for (const _wsElem of ${elementsVarName}) {
160
- const _wsVal = (_wsElem as any).${childProp};
175
+ const _wsVal = at(_wsElem, '${childProp}');
161
176
  if (_wsVal !== undefined && _wsVal !== null) {
162
177
  errors.push("${resType}.${relPath}:${sliceLabel}.${fhirPath}: max allowed = 0, but found " + (Array.isArray(_wsVal) ? _wsVal.length : 1));
163
178
  }
@@ -167,7 +182,7 @@ export function generateWithinSliceMaxChecks(children, elementsVarName, resType,
167
182
  // array field: check count doesn't exceed max
168
183
  code += `
169
184
  for (const _wsElem of ${elementsVarName}) {
170
- const _wsArr = (_wsElem as any).${childProp};
185
+ const _wsArr = at(_wsElem, '${childProp}');
171
186
  if (Array.isArray(_wsArr) && _wsArr.length > ${maxVal}) {
172
187
  errors.push("${resType}.${relPath}:${sliceLabel}.${fhirPath}: max allowed = ${maxVal}, but found " + _wsArr.length);
173
188
  }