import type { Resource } from 'fhir/r4'; import { readFieldValue } from './profileHistoryFields'; export interface FieldTimelineEntry { versionId?: string; value: unknown; whenIso?: string; who?: string; /** * `approx` - matched to a Provenance by timestamp. `unknown` - no Provenance describes this version. `refused` - we * were not permitted to read the Provenance, a fact about permission, never shown as `unknown`. `incomplete` - the * attribution was not all read, so no author is chosen from the part that was. */ whoConfidence?: 'approx' | 'unknown' | 'refused' | 'incomplete'; } export interface FieldTimeline { path: string; label: string; changed: boolean; entries: FieldTimelineEntry[]; } /** * Builds the change timeline for a single field across a resource's `_history` versions. * * `versions` MUST be newest-first (HealthLake `_history` order). Consecutive versions whose * value at `path` is identical (by `JSON.stringify`) are collapsed - `entries` holds only the * points where the field's value actually changed. `who`/`whoConfidence` are left unset here; * a later pass (the who-join) stamps them. * * EACH ENTRY IS DATED TO WHERE ITS RUN BEGAN, WHICH WALKING BACKWARDS IS THE **LAST** VERSION OF * THE RUN, NOT THE FIRST. * * The collapsing was always right and the date was always the wrong end. Keeping the first version * of each run - newest-first, the NEWEST version still carrying that value - dated every entry to * the last moment the value was still true rather than the moment it became true. So the current * value read as "changed just now" for ever, and moved again on every unrelated write to the * resource: a patient's marital status set in May reported itself as changed in September, then * moved to the following day when an unrelated profile save added a version. * * That is worse than vague, because it is precise. Per-field history is the product's answer to * what a clinician relied on and as of when, and a confident timestamp that is four months out is a * false answer rather than a weak one. It also manufactures apparent changes for fields nobody * touched. */ export function buildFieldTimeline(versions: Resource[], path: string, label: string): FieldTimeline { const entries: FieldTimelineEntry[] = []; let hasKept = false; let lastKeptKey: string | undefined; for (const version of versions) { const value = readFieldValue(version, path); const key = JSON.stringify(value); if (!hasKept || key !== lastKeptKey) { entries.push({ versionId: version.meta?.versionId, value, whenIso: version.meta?.lastUpdated, }); lastKeptKey = key; hasKept = true; } else { // Still inside the same run, and this version is OLDER than the one held - so it is a better // answer to "when did the field become this". Walking on to the end of the run leaves the // entry dated to where the run began. Entry ORDER is untouched: runs are still newest-first. const current = entries[entries.length - 1]; current.versionId = version.meta?.versionId; current.whenIso = version.meta?.lastUpdated; } } const changed = new Set(entries.map((e) => JSON.stringify(e.value))).size > 1; return { path, label, changed, entries }; }