// 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<string, string[]> = {
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<string, string>)[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<string, unknown> => !!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<T extends CheckResult>(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;
}