Spaces:
Running
Running
Juan Jimenez Carrero
feat: add mobile-first JS web app (Pollen's new Reachy Mini JS workflow)
a519434 | /** 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); | |