Skip to content

TypeSchema/TS/PY: keep a profile's reference targets within its base - #257

Draft
Palid wants to merge 16 commits into
atomic-ehr:mainfrom
Palid:typeschema/monotonic-reference-targets
Draft

Palid wants to merge 16 commits into
atomic-ehr:mainfrom
Palid:typeschema/monotonic-reference-targets

Conversation

@Palid

@Palid Palid commented Oct 1, 2026

Copy link
Copy Markdown

Stacked on #256 — review that first; this PR's diff against it is the last 8 commits. A profile that listed reference targets its base element does not allow got that list verbatim, so its input type was wider than the base field createResource assigns it to, and nothing reported it. Reference targets are now narrowed base-to-leaf the way #200 narrows choice variants, and the dropped targets are reported as generation warnings.

Closes #255

TypeSchema

  • resolvePermittedRefers in field-builder.ts folds each constraint level's reference targets base-to-leaf over the nearest specialization's targets
    • a target is admitted when its base resource lies within a permitted one, so Resource and DomainResource admit their descendants
    • an abstract target over a concrete set keeps the concrete permitted targets (Resource on Condition.subject keeps Patient, Group)
    • a level that admits none of its targets keeps the previous ones
  • resource and profiles are both built from the surviving targets, so a profile of a dropped resource leaves profiles too
  • new #nonMonotonicReference tag in src/utils/log.ts; the warning names the profile, the element and the dropped targets, and points at canonicalManager.patches
  • no writer changed: TypeScript's Raw type, setter, getter and validateReference list, and Python's annotations, validate_reference list and accessor comment all follow from the IR

A dropped target, and a level whose targets are all outside its ancestor's:

Profile 'WidenedRelatedPerson' (http://example.test/StructureDefinition/widened-related-person) declares reference target(s) http://hl7.org/fhir/StructureDefinition/Person on 'patient' that an ancestor prohibits; they stay prohibited (allowed: Patient). Fix the package with canonicalManager.patches
Profile 'PersonOnlyRelatedPerson' (http://example.org/StructureDefinition/PersonOnlyRelatedPerson) declares reference target(s) http://hl7.org/fhir/StructureDefinition/Person on 'patient', none of which an ancestor allows; the ancestor's targets are kept (allowed: Patient). Fix the package with canonicalManager.patches

gd-RelatedPerson without the norge-r4 patch failed tsc with TS2322; its input type now matches the base Reference<"Patient">, and the runtime check agrees with it:

 export type GdRelatedPersonProfileRaw = {
-    patient: Reference<"Person" /* http://ehelse.no/fhir/StructureDefinition/gd-Person, http://hl7.no/fhir/StructureDefinition/no-basis-Person */ | "Patient" /* http://hl7.no/fhir/StructureDefinition/no-basis-Patient */>;
+    patient: Reference<"Patient" /* http://hl7.no/fhir/StructureDefinition/no-basis-Patient */>;
 }
-                ...validateReference(res, profileName, "patient", ["Patient","Person"]),
+                ...validateReference(res, profileName, "patient", ["Patient"]),

Generation report

  • after the IR stage, APIBuilder.generate() copies the deduplicated #nonMonotonicReference and #nonMonotonicChoice messages of the ts logger into report.warnings, so prettyReport prints a Warnings: line
  • entries for tags in the logger's suppressTags are left out
  • success and .throwException() are unchanged: the narrowed output compiles, so the run succeeds
  • the CLI is unchanged

The norge-r4 example used to report a plain success; its Warnings: line now includes:

Profile 'SfmMedicationDispense' (http://ehelse.no/fhir/StructureDefinition/sfm-MedicationDispense) declares reference target(s) http://ehelse.no/fhir/StructureDefinition/sfm-MedicationStatement on 'authorizingPrescription' that an ancestor prohibits; they stay prohibited (allowed: MedicationRequest). Fix the package with canonicalManager.patches

Examples and tests

  • test/unit/typeschema/transformer/reference.test.ts: the first commit pins the widened targets, a later one asserts the narrowed ones and the warnings
    • widened top-level, choice-variant and nested-element targets
    • a narrowing of a family base, targets restated on an unconstrained base, an abstract target over a concrete set
    • a grandchild that allows none of its parent's targets
    • profile targets of an allowed and a dropped resource
  • test/unit/api/generation-report-warnings.test.ts: report.warnings, the prettyReport line, and a suppressed tag
  • the CDA describe in test/api/write-generator/typescript.test.ts asserts the ProtectiveFactor choice warning in result.warnings
  • examples/typescript-custom-packages:
    • widened-related-person.structuredefinition.json (R5, Patient and Person on RelatedPerson.patient), added as one tree-shake root
    • profile-reference-widening.test.ts: a demo building a valid related person, @ts-expect-error on a Person reference passed to createResource and setPatient, validate() rejecting one read from the wire, and a snapshot of the generated module

Reach

  • the only regenerated committed file is examples/python-r4-us-core/fhir_types/README.md: one line in its skipped-canonicals list, because that example loads the custom-packages structure-definitions/ folder
  • no other committed generated output changes
  • across the example corpora three profiles widen a reference target, all in norge-r4 (the three from TS: profile reference targets wider than the base emit a createResource that fails tsc (TS2322), with no diagnostic #255)
    • the gd-RelatedPerson patch stays; loader-level fixes remain the documented way
    • sfm-MedicationDispense and sfm-MedicationStatement are left unpatched: they now warn, and their validateReference lists lose MedicationStatement

Open question: should report.warnings honour suppressTags (it does here), or stay independent of the console filter?

Palid added 16 commits October 1, 2026 01:17
… locals

Records today's import blocks and validate() output for five causes:
- the ten-name validate bundle imported when one validator or none is called
- a complex extension resolving to a flat-input profile still imports its sub-extension value type (CodeableConcept)
- an extension slice without a url imports Extension and ensurePath unused
- a generic extension imports its profile class unused
- a profile with nothing to validate emits the profileName/res locals
- collectValidateBody collects the validate() expressions and the validate* helpers they call; the profile-helpers import takes its validate* names from that set, and validate() declares profileName/res only when it has an expression
- every other helper is imported under the condition its emitter uses: ensureSliceDefaults, setArraySlice/getArraySlice, isExtension, getExtensionValue, pushExtension, ensurePath and extractComplexExtension; isRawExtensionInput and ensureProfile no longer depend on the canonical url
- collectExtensionMethodInfos is the one list of url'd extensions the extension methods and the imports both read, so an extension without a url imports nothing
- a complex extension resolving to a flat-input profile no longer imports its sub-extension value types; a generic extension no longer imports its profile class
Flips the pin to the intended import blocks and the local-free empty validate(), and updates the snapshots whose profile modules lose unused helper and type imports.
… its params

generateType renders any schema named Reference, Coding or CodeableConcept with the hardcoded `<T extends string>` and drops its IR generic params, but generateDependenciesImports still imported the constraint type only those params named. The CDA Reference imported `Base` (r5) without using it.
Every example tsconfig extends the root one, so an unused import in generated output now fails the example typecheck. Drops the unused test fixtures it flags (_dateTimeType, _periodType, _rangeType, _basePackageInfo) and writes the vflat member check in _flatContractTypes as an expression.
- examples/typescript-r4-us-core/fhir-types: 17 profile modules
- examples/typescript-custom-packages/__snapshots__: profile-complex-extension-flat, profile-typed-bundle
- examples/on-the-fly/norge-r4/__snapshots__: kjernejournal-profiles
- examples/on-the-fly/ccda/__snapshots__: uv-extensions-r4
- RelatedPerson.patient widened to Patient and Person
- Provenance.target narrowed from Resource to Patient and Device
- Extension.valueReference restated with targets on an unconstrained base
- MedicationRequest.reportedReference with an extra Device
- Encounter.participant.individual with an extra Patient
- a grandchild of the widened profile restating Person only
- local Patient and Person profiles as targets alongside Patient
- an in-memory generate() over a widened RelatedPerson reports no warnings
- fold each constraint level's reference targets base-to-leaf over the nearest specialization's targets
- admit a target when its base resource lies within a permitted one, so Resource and DomainResource admit their descendants
- keep the previous targets when a level admits none
- warn with #nonMonotonicReference naming the profile, the path and the dropped targets
- emit resource and profiles from the surviving targets
- copy the deduplicated #nonMonotonicReference and #nonMonotonicChoice messages of the ts logger into report.warnings after the IR stage
- widened top-level, choice-variant and nested targets are dropped with a warning
- a grandchild that allows none of its parent's targets keeps them with a distinct warning
- profiles of a dropped resource leave profiles too
- generate() reports the warning and prettyReport prints it
- WidenedRelatedPerson (R5) restates RelatedPerson.patient with Patient and Person, added as a tree-shake root
- demo builds a valid related person for a Patient
- a Person reference fails tsc in createResource and setPatient, and validate() rejects one read from the wire
- snapshot of the generated profile module
…ings

- a Condition profile restating subject as Resource and Patient keeps Patient and Group and warns about Resource
- the CDA fixture's prohibited choice variants reach report.warnings
@Palid
Palid marked this pull request as draft October 1, 2026 02:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TS: profile reference targets wider than the base emit a createResource that fails tsc (TS2322), with no diagnostic

1 participant