mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
refactor(profile): route every binary-sex narrow through the one owner
`binaryReferenceSex` was already the declared owner of the shrink from the three stored values to the two a sex-split reference table has rows for, but eight sites still narrowed inline — including a byte-for-byte private copy in the effective-range resolver and a ternary 37 lines above a `toProfileSex` call in the same file. Each of those is a place the fallback can drift. Delete the copy, replace the ternaries, and name the pair once as `BinaryReferenceSex` so a signature that needs it stops re-listing the two literals. No behaviour changes: every site already resolved OTHER and "no answer" to the same neutral or absent value, and the existing suites pass untouched.
This commit is contained in:
1 parent
eba9f15539
commit
36f525c465
9 files changed
+36
-26
No files matched your search
@@ -22,6 +22,7 @@ import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { binaryReferenceSex } from "@/lib/profile/sex";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import {
|
||||
NUTRIENT_CATALOG,
|
||||
@@ -72,8 +73,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
daySeries.push({ day: key, amount: sumByDay.get(key) ?? 0 });
|
||||
}
|
||||
|
||||
const sex =
|
||||
user.gender === "MALE" || user.gender === "FEMALE" ? user.gender : null;
|
||||
const sex = binaryReferenceSex(user.gender);
|
||||
const reference = resolveNutrientReference(nutrient, sex);
|
||||
|
||||
annotate({
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { binaryReferenceSex } from "@/lib/profile/sex";
|
||||
import {
|
||||
NUTRIENT_CODES,
|
||||
isNutrientCode,
|
||||
@@ -51,8 +52,7 @@ export function MicronutrientsCard() {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
const { user } = useAuth();
|
||||
const sex =
|
||||
user?.gender === "MALE" || user?.gender === "FEMALE" ? user.gender : null;
|
||||
const sex = binaryReferenceSex(user?.gender);
|
||||
|
||||
// A failed read left `data` undefined, `rows` empty, and the card rendered
|
||||
// its "no micronutrient data" EmptyState — the same output as an account that
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
* The band position is computed HERE, server-side — never left to the model.
|
||||
*/
|
||||
|
||||
import { binaryReferenceSex, type BinaryReferenceSex } from "@/lib/profile/sex";
|
||||
|
||||
/** How a band reads against health: the four-way valence the prose leans on. */
|
||||
export type BandValence = "favourable" | "neutral" | "caution" | "unfavourable";
|
||||
|
||||
@@ -54,7 +56,7 @@ export interface MetricInterpretation {
|
||||
/** Ascending bands when the metric is not sex-split. */
|
||||
bands?: InterpretationBand[];
|
||||
/** Sex-split bands; used only when the profile carries the sex. */
|
||||
sexBands?: Record<"MALE" | "FEMALE", InterpretationBand[]>;
|
||||
sexBands?: Record<BinaryReferenceSex, InterpretationBand[]>;
|
||||
/**
|
||||
* Optional honesty caveat surfaced in the prompt block — used where the KB
|
||||
* hedges the edges (consumer rating scales, proxy measurements).
|
||||
@@ -300,7 +302,7 @@ const INTERPRETATION_REGISTRY: Record<string, MetricInterpretation> = {
|
||||
*/
|
||||
export function resolveInterpretation(
|
||||
metricKey: string,
|
||||
sex: "MALE" | "FEMALE" | null | undefined,
|
||||
sex: BinaryReferenceSex | null | undefined,
|
||||
): {
|
||||
unit: string;
|
||||
directionOfGood: DirectionOfGood;
|
||||
@@ -312,8 +314,9 @@ export function resolveInterpretation(
|
||||
if (!entry) return null;
|
||||
let bands: InterpretationBand[] | undefined = entry.bands;
|
||||
if (entry.sexBands) {
|
||||
if (sex !== "MALE" && sex !== "FEMALE") return null;
|
||||
bands = entry.sexBands[sex];
|
||||
const referenceSex = binaryReferenceSex(sex);
|
||||
if (!referenceSex) return null;
|
||||
bands = entry.sexBands[referenceSex];
|
||||
}
|
||||
if (!bands || bands.length === 0) return null;
|
||||
return {
|
||||
@@ -398,7 +401,7 @@ export function classifyBandPosition(
|
||||
*/
|
||||
export function interpretationBandEdges(
|
||||
metricKey: string,
|
||||
sex?: "MALE" | "FEMALE" | null,
|
||||
sex?: BinaryReferenceSex | null,
|
||||
): number[] {
|
||||
const resolved = resolveInterpretation(metricKey, sex ?? null);
|
||||
if (!resolved) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
* consistently.
|
||||
*/
|
||||
|
||||
import { binaryReferenceSex } from "@/lib/profile/sex";
|
||||
import { getBpTargets } from "./bp-targets";
|
||||
import {
|
||||
getAgeFromDateOfBirth,
|
||||
@@ -120,11 +121,6 @@ const GLUCOSE_DEFAULTS: Record<
|
||||
BEDTIME: { min: 90, max: 150 },
|
||||
};
|
||||
|
||||
function normalizedGender(value: string | null): "MALE" | "FEMALE" | null {
|
||||
if (value === "MALE" || value === "FEMALE") return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
function toDate(value: Date | string | null): Date | null {
|
||||
if (!value) return null;
|
||||
return value instanceof Date ? value : new Date(value);
|
||||
@@ -135,7 +131,7 @@ function defaultRange(
|
||||
profile: UserProfileForRange,
|
||||
): TrafficRange | null {
|
||||
const age = getAgeFromDateOfBirth(profile.dateOfBirth ?? null);
|
||||
const gender = normalizedGender(profile.gender ?? null);
|
||||
const gender = binaryReferenceSex(profile.gender);
|
||||
const dateOfBirth = toDate(profile.dateOfBirth);
|
||||
|
||||
switch (metric) {
|
||||
|
||||
@@ -101,7 +101,11 @@ import {
|
||||
import { getServerTranslator } from "@/lib/i18n/server-translator";
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { getAgeFromDateOfBirth } from "@/lib/analytics/pulse-targets";
|
||||
import { toProfileSex, type ProfileSex } from "@/lib/profile/sex";
|
||||
import {
|
||||
binaryReferenceSex,
|
||||
toProfileSex,
|
||||
type ProfileSex,
|
||||
} from "@/lib/profile/sex";
|
||||
|
||||
/** Briefing freshness window — mirrors the 24 h TTL on the advisor cache. */
|
||||
const BRIEFING_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
@@ -1246,8 +1250,7 @@ export async function buildDashboardSnapshot(
|
||||
// never sinks the snapshot. The narrative block rides the warm phase's
|
||||
// healthScore; memory rides the briefing card. The locale defaults to English.
|
||||
const locale = options.locale ?? "en";
|
||||
const narrativeSex: "MALE" | "FEMALE" | null =
|
||||
user.gender === "MALE" || user.gender === "FEMALE" ? user.gender : null;
|
||||
const narrativeSex = binaryReferenceSex(user.gender);
|
||||
const narrativeProfile = {
|
||||
ageYears: getAgeFromDateOfBirth(user.dateOfBirth),
|
||||
sex: narrativeSex,
|
||||
|
||||
@@ -83,6 +83,7 @@ import {
|
||||
import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash";
|
||||
import { returnTimeoutFallback } from "@/lib/insights/timeout-fallback";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { binaryReferenceSex } from "@/lib/profile/sex";
|
||||
|
||||
export interface MetricStatusResult {
|
||||
hasProvider: boolean;
|
||||
@@ -335,8 +336,7 @@ export async function generateMetricStatus(args: {
|
||||
: null;
|
||||
|
||||
const ageYears = getAgeFromDateOfBirth(user?.dateOfBirth ?? null);
|
||||
const sex =
|
||||
user?.gender === "MALE" || user?.gender === "FEMALE" ? user.gender : null;
|
||||
const sex = binaryReferenceSex(user?.gender);
|
||||
|
||||
// v1.10.0 F1 — sharpen the coarse registry `normalRange` with the
|
||||
// age/sex reference-range enabler when one is available for this
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
import { prisma } from "@/lib/db";
|
||||
import { isModuleEnabled } from "@/lib/modules/gate";
|
||||
import { binaryReferenceSex, type BinaryReferenceSex } from "@/lib/profile/sex";
|
||||
import {
|
||||
NUTRIENT_CATALOG,
|
||||
NUTRIENT_CODES,
|
||||
@@ -195,7 +196,7 @@ async function readOverview(
|
||||
async function readDaily(
|
||||
userId: string,
|
||||
userTz: string,
|
||||
sex: "MALE" | "FEMALE" | null,
|
||||
sex: BinaryReferenceSex | null,
|
||||
nutrient: NutrientCode,
|
||||
days: number,
|
||||
): Promise<NutrientsDailyResult> {
|
||||
@@ -268,9 +269,7 @@ export async function getNutrients(
|
||||
select: { timezone: true, gender: true },
|
||||
});
|
||||
const userTz = user?.timezone || DEFAULT_TIMEZONE;
|
||||
const genderValue = user?.gender ?? null;
|
||||
const sex =
|
||||
genderValue === "MALE" || genderValue === "FEMALE" ? genderValue : null;
|
||||
const sex = binaryReferenceSex(user?.gender);
|
||||
|
||||
if (!args.nutrient) {
|
||||
const days = clampDays(args.days, OVERVIEW_DEFAULT_DAYS, OVERVIEW_MAX_DAYS);
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
* its source string.
|
||||
*/
|
||||
|
||||
import type { BinaryReferenceSex } from "@/lib/profile/sex";
|
||||
|
||||
export const NUTRIENT_CODES = [
|
||||
// 13 vitamins
|
||||
"vitamin_a",
|
||||
@@ -331,7 +333,7 @@ export interface ResolvedNutrientReference {
|
||||
*/
|
||||
export function resolveNutrientReference(
|
||||
code: NutrientCode,
|
||||
sex: "MALE" | "FEMALE" | null,
|
||||
sex: BinaryReferenceSex | null,
|
||||
): ResolvedNutrientReference | null {
|
||||
const ref = NUTRIENT_CATALOG[code].reference;
|
||||
const value =
|
||||
|
||||
@@ -20,6 +20,13 @@
|
||||
/** Profile sex exactly as stored; `null` = the account gave no answer. */
|
||||
export type ProfileSex = "MALE" | "FEMALE" | "OTHER" | null;
|
||||
|
||||
/**
|
||||
* The two values a sex-split reference table actually has rows for. Named so
|
||||
* a table's signature can say what it needs without re-listing the pair, and
|
||||
* so the list has exactly one place to change.
|
||||
*/
|
||||
export type BinaryReferenceSex = "MALE" | "FEMALE";
|
||||
|
||||
/**
|
||||
* Narrow a stored sex to the two values a sex-split reference table has
|
||||
* rows for. `OTHER` and "no answer" both resolve to `null`, and every
|
||||
@@ -29,7 +36,7 @@ export type ProfileSex = "MALE" | "FEMALE" | "OTHER" | null;
|
||||
*/
|
||||
export function binaryReferenceSex(
|
||||
sex: string | null | undefined,
|
||||
): "MALE" | "FEMALE" | null {
|
||||
): BinaryReferenceSex | null {
|
||||
return sex === "MALE" || sex === "FEMALE" ? sex : null;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user