// Patients have no pronoun on file, and Context never guesses one. The engine words a person by // the sex recorded ("her Penicillin allergy"), so Context takes its wording from the same check // run with no sex given, string by string, and only where the two differ in pronouns alone: // tiers, findings, doses and every other word stay as the real check made them. Pure. import type { CheckInput, CheckResult } from '../../domain/checks/engine.js'; const NEUTRAL: Record = { her: ['their', 'them'], his: ['their'], him: ['them'], hers: ['theirs'], she: ['they'], he: ['they'], herself: ['themselves'], himself: ['themselves'], }; const SUBJECTS = new Set(['she', 'he']); /** "she feels" becomes "they feel": the verb after a subject pronoun may change with it. */ const VERB = (gendered: string, neutral: string) => ({ is: 'are', was: 'were', has: 'have', does: 'do', "doesn't": "don't", "isn't": "aren't", "wasn't": "weren't", "hasn't": "haven't" } as Record)[gendered] === neutral || (gendered.endsWith('s') && gendered.slice(0, -1) === neutral); const words = (text: string) => text.split(/([^\p{L}']+)/u); /** True when the neutral string is the gendered one with only its pronouns (and their verbs) changed. */ export function onlyPronounsDiffer(gendered: string, neutral: string): boolean { if (gendered === neutral) return false; const a = words(gendered); const b = words(neutral); if (a.length !== b.length) return false; let afterSubject = false; for (let i = 0; i < a.length; i++) { if (a[i] === b[i]) { if (/\p{L}/u.test(a[i])) afterSubject = false; continue; } const from = a[i].toLowerCase(); const to = b[i].toLowerCase(); if (NEUTRAL[from]?.includes(to)) { afterSubject = SUBJECTS.has(from); continue; } if (afterSubject && VERB(from, to)) { afterSubject = false; continue; } return false; } return true; } const isObject = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value); const idOf = (value: unknown) => (isObject(value) && typeof value.id === 'string' ? value.id : null); function reword(real: unknown, neutral: unknown): unknown { if (typeof real === 'string') return typeof neutral === 'string' && onlyPronounsDiffer(real, neutral) ? neutral : real; if (Array.isArray(real)) { if (!Array.isArray(neutral)) return real; // Findings are matched by id; anything else by position, and only when the lists line up. if (real.every((item) => idOf(item) !== null)) return real.map((item) => reword(item, neutral.find((other) => idOf(other) === idOf(item)))); return real.length === neutral.length ? real.map((item, index) => reword(item, neutral[index])) : real; } if (isObject(real)) { if (!isObject(neutral)) return real; return Object.fromEntries(Object.entries(real).map(([key, value]) => [key, reword(value, neutral[key])])); } return real; } /** The check, worded without a guessed he or she. With no sex recorded it is the check as it is. */ export function withoutGuessedPronouns(input: CheckInput, result: T, check: (input: CheckInput) => T): T { if (!input.subject.sex) return result; return reword(result, check({ ...input, subject: { ...input.subject, sex: null } })) as T; }