// utilBuildReferenceResolvabilityDistribution - build/write/status a reference resolvability distribution audit output // Purpose: provide one reusable pipeline for reference audits (analyze -> resolve -> // aggregate -> write output -> set status) while allowing per-audit configuration. // Input: // { // sampleTypes: string|[string], // elementPath: string, // allowedReferenceTypes: string|[string], // outputFileName: string, // statusEntityLabel [optional]: string, // totalCategoryLabel [optional]: string, // noSamplesMessage [optional]: string, // includeReferencePercentages [optional]: boolean, // includeExColumn [optional]: boolean // } ( $assert($type($) = 'object', 'utilBuildReferenceResolvabilityDistribution: input object is required'); $assert($exists(sampleTypes), 'utilBuildReferenceResolvabilityDistribution: input.sampleTypes is required'); $assert( $exists(elementPath) and $type(elementPath) = 'string' and $trim(elementPath) != '', 'utilBuildReferenceResolvabilityDistribution: input.elementPath (string) is required' ); $assert($exists(allowedReferenceTypes), 'utilBuildReferenceResolvabilityDistribution: input.allowedReferenceTypes is required'); $assert($exists(outputFileName) and $type(outputFileName) = 'string', 'utilBuildReferenceResolvabilityDistribution: input.outputFileName is required'); $toArray := function($value) {( [$value] )}; $sampleTypeList := $toArray(sampleTypes)[$type($) = 'string']; $allowedTypes := $toArray(allowedReferenceTypes)[$type($) = 'string']; $statusEntity := $exists(statusEntityLabel) and $statusEntityLabel != '' ? statusEntityLabel : ($count($sampleTypeList) > 0 ? $sampleTypeList[0] : 'Resource'); $totalLabel := $exists(totalCategoryLabel) and totalCategoryLabel != '' ? totalCategoryLabel : ('Total ' & $statusEntity & ' Instances in sample'); $noSamplesText := $exists(noSamplesMessage) and noSamplesMessage != '' ? noSamplesMessage : ('No sampled ' & $statusEntity & ' resources found'); $withRefPercentages := $exists(includeReferencePercentages) ? includeReferencePercentages = true : true; $withExColumn := $exists(includeExColumn) ? includeExColumn = true : false; $resolveElementByPath := function($resource, $path) {( $parts := $split($path, '.'); $reduce($parts, function($acc, $part) { $exists($acc) ? $lookup($acc, $part) : undefined }, $resource) )}; $selectElement := function($resource) {( $resolveElementByPath($resource, elementPath) )}; $sampleFiles := $utilGetAllSampledResourceFilenames($readDir(), {'types': $sampleTypeList}); $totalSamples := $count($sampleFiles); // Streaming function: read one resource, analyze its reference element, and // reduce it to a small record immediately - the full resource body is not // retained after this returns. (Same pattern as utilExtractIdentifiers / // utilExtractPatientIdentifiers - avoids holding every sampled resource body // in memory at once, which was causing heap-OOM on large/heavy resource // types such as Encounter.) $analyzeFile := function($filename) {( $resource := $readFile($filename); $elementAnalysis := $utilAnalyzeReferenceElement({ 'element': $selectElement($resource), 'allowedTypes': $allowedTypes }); { 'resourceId': $resource.id, 'referenceCount': $elementAnalysis.referenceCount, 'hasDAR': $elementAnalysis.hasDAR, 'parsedReferences': $elementAnalysis.parsedReferences, 'hasInvalidType': $elementAnalysis.invalidTypeCount > 0 } )}; // Split files across 10 lanes for parallel streaming (same pattern as utilExtractIdentifiers) $lane := function($laneIndex) { $filter($sampleFiles, function($v, $i){ ($i % 10) = $laneIndex }).$analyzeFile($) }; $lanes := [ $lane(0), $lane(1), $lane(2), $lane(3), $lane(4), $lane(5), $lane(6), $lane(7), $lane(8), $lane(9) ]; $analysis := $lanes[]; $distinctResolvableReferences := $distinct($analysis.parsedReferences[isParsed = true and isValidType = true].reference); // Bound concurrency of live resolvability checks. Each one is a full HTTP // round-trip (base-URL discovery + metadata + the actual GET, each with up // to 5 retries/exponential backoff) via $utilIsReferenceResolvable -> // $FHIRQueryWithPaging. Resolving every distinct reference at once left // thousands of retrying requests in flight simultaneously against a // slow/unresponsive FHIR server - that pending-request/retry-timer pileup, // not the (tiny) resource bodies read above, is what was exhausting the // heap. Concurrency is capped lower than the 10-lane file-read pattern // above since each unit of work here is a network call with its own // retry/backoff chain, not a cheap local file read. $resolveConcurrency := 5; $resolveOneReference := function($ref) {( { 'reference': $ref, 'isResolvable': $utilIsReferenceResolvable($ref) } )}; $resolveLane := function($laneIndex) { $filter($distinctResolvableReferences, function($v, $i){ ($i % $resolveConcurrency) = $laneIndex }).$resolveOneReference($) }; $resolvabilityResults := [ $resolveLane(0), $resolveLane(1), $resolveLane(2), $resolveLane(3), $resolveLane(4) ][]; $resolvabilityLookup := $resolvabilityResults{reference: isResolvable}; $enrichedAnalysis := $analysis.( $enrichedRefs := parsedReferences.( $canResolve := isParsed and isValidType; $merge([$, { 'isResolvable': $canResolve ? $lookup($resolvabilityLookup, reference) : undefined }]) ); $merge([$, { 'enrichedReferences': $enrichedRefs }]) ); $pct := function($countValue) {( $totalSamples > 0 ? $round(($countValue / $totalSamples) * 100, 1) & '%' : '0%' )}; $pctOf := function($countValue, $totalValue) {( $totalValue > 0 ? $round(($countValue / $totalValue) * 100, 1) & '%' : '0%' )}; $sampleIdsText := function($ids) {( $normalizedIds := $toArray($ids); $distinctIds := $distinct($normalizedIds[$type($) = 'string' and $trim($) != '']); $topIds := $toArray($distinctIds[[0..4]]); $joined := $join($topIds, ', '); $exists($joined) ? $joined : '' )}; $buildSplitExampleText := function($resolvableIds, $nonResolvableIds) {( $resText := $sampleIdsText($resolvableIds); $nonResText := $sampleIdsText($nonResolvableIds); ($resText != '' and $nonResText != '') ? ('resolvable: ' & $resText & ' | non-resolvable: ' & $nonResText) : ($resText != '' ? $resText : $nonResText) )}; $buildRowForType := function($typeName) {( $typeInstances := $enrichedAnalysis[$count(enrichedReferences[targetResourceType = $typeName]) > 0]; $instanceCount := $count($typeInstances); $resolvableCount := $count($enrichedAnalysis.enrichedReferences[targetResourceType = $typeName and isResolvable = true]); $nonResolvableCount := $count($enrichedAnalysis.enrichedReferences[targetResourceType = $typeName and isResolvable = false]); $typeReferenceTotal := $resolvableCount + $nonResolvableCount; $resolvableInstanceIds := $typeInstances[$count(enrichedReferences[targetResourceType = $typeName and isResolvable = true]) > 0].resourceId; $nonResolvableInstanceIds := $typeInstances[$count(enrichedReferences[targetResourceType = $typeName and isResolvable = false]) > 0].resourceId; $merge([ { 'category': $typeName & ' Referenced', 'count': $instanceCount, 'percentage': $pct($instanceCount), 'resolvableReferenceCount': $resolvableCount, 'nonResolvableReferenceCount': $nonResolvableCount }, $withRefPercentages ? { 'resolvableReferencePercentage': $pctOf($resolvableCount, $typeReferenceTotal), 'nonResolvableReferencePercentage': $pctOf($nonResolvableCount, $typeReferenceTotal) } : {}, $withExColumn ? {'ex': $buildSplitExampleText($resolvableInstanceIds, $nonResolvableInstanceIds)} : {} ]) )}; $multipleRefInstanceCount := $count($enrichedAnalysis[referenceCount > 1]); $invalidTypeInstanceCount := $count($enrichedAnalysis[hasInvalidType = true]); $invalidTypeReferenceCount := $count($enrichedAnalysis.enrichedReferences[$not(isValidType)]); $nonResolvableAllowedRefCount := $count($enrichedAnalysis.enrichedReferences[isValidType = true and isResolvable = false]); $darCount := $count($enrichedAnalysis[referenceCount = 0 and hasDAR = true]); $missingCount := $count($enrichedAnalysis[referenceCount = 0 and hasDAR = false]); $rowsByType := [$allowedTypes.$buildRowForType($)][]; $multipleRefInstanceIds := $enrichedAnalysis[referenceCount > 1].resourceId; $invalidTypeInstanceIds := $enrichedAnalysis[hasInvalidType = true].resourceId; $darInstanceIds := $enrichedAnalysis[referenceCount = 0 and hasDAR = true].resourceId; $missingInstanceIds := $enrichedAnalysis[referenceCount = 0 and hasDAR = false].resourceId; $allInstanceIds := $enrichedAnalysis.resourceId; $naRowMerge := $merge([ { 'resolvableReferenceCount': 'N/A', 'nonResolvableReferenceCount': 'N/A' }, $withRefPercentages ? { 'resolvableReferencePercentage': 'N/A', 'nonResolvableReferencePercentage': 'N/A' } : {} ]); $finalTable := [ $rowsByType, $merge([{ 'category': 'More than one Reference', 'count': $multipleRefInstanceCount, 'percentage': $pct($multipleRefInstanceCount), 'ex': $withExColumn ? $sampleIdsText($multipleRefInstanceIds) : undefined }, $naRowMerge]), $merge([{ 'category': 'Reference to Resource not in List', 'count': $invalidTypeInstanceCount, 'percentage': $pct($invalidTypeInstanceCount), 'ex': $withExColumn ? $sampleIdsText($invalidTypeInstanceIds) : undefined }, $naRowMerge]), $merge([{ 'category': 'DAR', 'count': $darCount, 'percentage': $pct($darCount), 'ex': $withExColumn ? $sampleIdsText($darInstanceIds) : undefined }, $naRowMerge]), $merge([{ 'category': 'Missing', 'count': $missingCount, 'percentage': $pct($missingCount), 'ex': $withExColumn ? $sampleIdsText($missingInstanceIds) : undefined }, $naRowMerge]), $merge([{ 'category': $totalLabel, 'count': $totalSamples, 'percentage': '100%', 'ex': $withExColumn ? $sampleIdsText($allInstanceIds) : undefined }, $naRowMerge]) ][*]; $writeFile($finalTable, outputFileName); $invalidOrMissingIssueCount := $invalidTypeReferenceCount + $darCount + $missingCount; $totalSamples = 0 ? $setStatus( 'error', 'Error', $noSamplesText ) : $setStatus( 'completed', 'Completed', 'Analyzed ' & $string($totalSamples) & ' ' & $statusEntity & ' instance(s); ' & 'issues summary: non-resolvable=' & $string($nonResolvableAllowedRefCount) & ', invalid-type=' & $string($invalidTypeReferenceCount) & ', DAR(no reference)=' & $string($darCount) & ', missing(no reference)=' & $string($missingCount) & ', totalIssueBuckets=' & $string($nonResolvableAllowedRefCount + $invalidOrMissingIssueCount) ) )