mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
Add affine-correctness tests for the transform registry (°C→°F absolute vs a delta that must stay offset-free, lb/in factors, inverse round-trips), a resolver test for the inbound unit aliases, a preferred-unit entry-form label test, and MCP cases for converting a lb hint to canonical kg and refusing an unknown unit. Add the structural guard that trips if any dashboard or insight surface reintroduces a hardcoded transformed-type unit or value scale. Ship the diagnose/convert script pair for legacy non-canonical measurement rows.
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
/**
|
|
* Read-only diagnostic for measurement rows stored in a NON-canonical unit.
|
|
*
|
|
* Canonical storage is SI: every write path stamps `getUnitForType(type)`. The
|
|
* one historical leak was the MCP `log_measurement` tool, which persisted a
|
|
* caller-supplied `unit` verbatim (closed in v1.32.26). A row whose `unit`
|
|
* column differs from its type's canonical unit is such a legacy row, and it
|
|
* mislabels every display surface that trusts the column.
|
|
*
|
|
* This groups measurements by `(type, unit)` and prints every group whose
|
|
* `unit` is not the canonical unit for its type, flagging whether the stored
|
|
* unit is a RECOGNISED alias (mechanically convertible by the companion
|
|
* convert script) or UNKNOWN (needs a manual look). Nothing is written.
|
|
*
|
|
* NOT auto-fixable and deliberately out of scope: a row whose stored unit IS
|
|
* already canonical but whose VALUE was a foreign unit (e.g. a lb reading typed
|
|
* into the old kg-labelled entry form). Those are indistinguishable from real
|
|
* canonical readings; the reporter edits them via the record list.
|
|
*
|
|
* Run (inside the app container, which has DATABASE_URL):
|
|
* pnpm dlx tsx scripts/diagnose-noncanonical-measurement-units.ts
|
|
*/
|
|
import "dotenv/config";
|
|
|
|
import { prisma } from "@/lib/db";
|
|
import { getUnitForType } from "@/lib/validations/measurement";
|
|
import { resolveToCanonicalUnit } from "@/lib/measurements/unit-aliases";
|
|
|
|
async function main(): Promise<void> {
|
|
const groups = await prisma.measurement.groupBy({
|
|
by: ["type", "unit"],
|
|
_count: { _all: true },
|
|
});
|
|
|
|
const mismatches = groups.filter((g) => {
|
|
if (g.unit === null) return false;
|
|
return g.unit !== getUnitForType(g.type);
|
|
});
|
|
|
|
if (mismatches.length === 0) {
|
|
console.log("No non-canonical measurement units found. Storage is clean.");
|
|
return;
|
|
}
|
|
|
|
let convertible = 0;
|
|
let unknown = 0;
|
|
console.log(
|
|
`Found ${mismatches.length} non-canonical (type, unit) group(s):\n`,
|
|
);
|
|
for (const g of mismatches.sort((a, b) => b._count._all - a._count._all)) {
|
|
const canonical = getUnitForType(g.type);
|
|
// Probe convertibility with a neutral value — only the recognition matters.
|
|
const recognised = resolveToCanonicalUnit(g.type, 1, g.unit as string);
|
|
const status = recognised ? "CONVERTIBLE" : "UNKNOWN — manual review";
|
|
if (recognised) convertible += g._count._all;
|
|
else unknown += g._count._all;
|
|
console.log(
|
|
` ${g.type}: stored="${g.unit}" canonical="${canonical}" ` +
|
|
`rows=${g._count._all} [${status}]`,
|
|
);
|
|
}
|
|
console.log(
|
|
`\nTotals: ${convertible} row(s) convertible, ${unknown} row(s) unknown.`,
|
|
);
|
|
console.log(
|
|
"Convert the recognised aliases with:\n" +
|
|
" pnpm dlx tsx scripts/convert-noncanonical-measurement-units.ts --execute",
|
|
);
|
|
}
|
|
|
|
main()
|
|
.catch((err) => {
|
|
console.error(err);
|
|
process.exitCode = 1;
|
|
})
|
|
.finally(() => prisma.$disconnect());
|