cookAIware / web /scripts /parity-check.ts
Juan Jimenez Carrero
feat: add mobile-first JS web app (Pollen's new Reachy Mini JS workflow)
a519434
Raw
History Blame Contribute Delete
3.6 kB
/** Domain parity check: run the TS port against the repo's real Python data
* files and verify schema + shortfall math agree with the Python outputs.
* Run: npx -y tsx scripts/parity-check.ts */
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { generatePlan } from "../src/domain/mealPlanner";
import { generateFromPlan } from "../src/domain/shoppingList";
import { formatQuantity, normalizeQuantity } from "../src/domain/units";
import type { InventoryItem, MealPlan, ShoppingItem, FamilyProfile } from "../src/domain/types";
const dataDir = join(dirname(fileURLToPath(import.meta.url)), "../../src/cookAIware/data");
const read = <T>(file: string): T => JSON.parse(readFileSync(join(dataDir, file), "utf-8")) as T;
const inventory = read<InventoryItem[]>("inventory.json");
const pyPlan = read<MealPlan>("meal_plan.json");
const profile = read<FamilyProfile>("family_profile.json");
// Captured from running cookAIware.shopping_list.generate_from_plan on the
// same inputs (the committed shopping_list.json was edited after generation).
const pyShopping = JSON.parse(
readFileSync(join(dirname(fileURLToPath(import.meta.url)), "fixtures/expected-shopping.json"), "utf-8"),
) as ShoppingItem[];
let failures = 0;
const check = (name: string, ok: boolean, detail = "") => {
console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " — " + detail : ""}`);
if (!ok) failures++;
};
// 1. Units parity
check("normalizeQuantity kg→g", JSON.stringify(normalizeQuantity(1.5, "kg")) === JSON.stringify([1500, "g"]));
check("formatQuantity 1600 g", formatQuantity(1600, "g") === "1.60 kg");
check("formatQuantity 3 pcs", formatQuantity(3, "pcs") === "3 pcs");
// 2. Shopping list: TS shortfall math over the *Python-generated* plan and
// inventory must reproduce the Python shopping_list.json exactly.
const tsShopping = generateFromPlan(pyPlan, inventory);
const norm = (items: ShoppingItem[]) =>
[...items]
.map((item) => `${item.name}|${item.unit}|${Math.round(item.quantity * 100) / 100}`)
.sort()
.join("\n");
check(
"shopping list matches Python output",
norm(tsShopping) === norm(pyShopping),
`ts=${tsShopping.length} py=${pyShopping.length}`,
);
// 3. Plan generation: same schema as the Python plan
const tsPlan = generatePlan(inventory, profile.adults ?? 2, profile.children ?? 0, profile.schedule);
check("plan has 7 days", tsPlan.days.length === 7);
const pyDayShape = pyPlan.days.map((d) => `${d.day}:${d.meals.map((m) => m.meal).join("+")}`).join(" ");
const tsDayShape = tsPlan.days.map((d) => `${d.day}:${d.meals.map((m) => m.meal).join("+")}`).join(" ");
check("meal slots match Python schedule", pyDayShape === tsDayShape, tsDayShape);
const sampleMeal = tsPlan.days.flatMap((d) => d.meals).find((m) => m.ingredients.length > 0);
check(
"ingredient entries have Python schema keys",
!!sampleMeal &&
sampleMeal.ingredients.every(
(ing) =>
typeof ing.name === "string" &&
typeof ing.display_name === "string" &&
typeof ing.quantity === "number" &&
typeof ing.unit === "string" &&
typeof ing.display_quantity === "string",
),
);
const servingsAll = (profile.adults ?? 0) + (profile.children ?? 0);
const dinner = tsPlan.days[0].meals.find((m) => m.meal === "dinner");
check("dinner servings = adults+children", dinner?.servings === servingsAll, `got ${dinner?.servings}`);
console.log(failures === 0 ? "\nALL PARITY CHECKS PASSED" : `\n${failures} CHECK(S) FAILED`);
process.exit(failures === 0 ? 0 : 1);