/** 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 = (file: string): T => JSON.parse(readFileSync(join(dataDir, file), "utf-8")) as T; const inventory = read("inventory.json"); const pyPlan = read("meal_plan.json"); const profile = read("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);