Drop .cursor rules and npm lockfile; standardize on Bun; remove v1 migration code

- Delete .cursor/rules/boardui.mdc (rules live in AGENTS.md) and
  package-lock.json; README now uses bun install/dev/test/build
- Remove migrateV1Slots, the v1 localStorage key, the legacy "asian"
  cuisine expansion, the ownsPantryStaples fallback, and the pre-BoardUI
  theme-key carryover in the layout init script; drop their tests

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Cayden Schwechten 2026-11-06 20:41:36 -06:00
parent df5dd3a70e
commit 2cd683e64a
7 changed files with 14 additions and 6904 deletions

View file

@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test";
import type { MealPlanState } from "@/app/types";
import { activeRecipeIds, mealKindFor, mealLabel, visibleMeals } from "./plan";
import { DEFAULT_STATE, migrateV1Slots, sanitizeState } from "./sanitize";
import { DEFAULT_STATE } from "./sanitize";
import { RECIPES } from "./recipes";
import { planReducer } from "./usePlanState";
@ -70,24 +70,3 @@ describe("Clear", () => {
expect(cleared.slots).toEqual({ "0-1": PANCAKES }); // breakfast was hidden, so untouched
});
});
describe("migrating plans saved before meal numbers were fixed", () => {
test("2 meals/day: old 1/2 (Lunch/Dinner) become 2/3", () => {
const old = { mealsPerDay: 2, slots: { "0-1": SANDWICH, "0-2": STEW } };
expect((migrateV1Slots(old) as MealPlanState).slots).toEqual({ "0-2": SANDWICH, "0-3": STEW });
});
test("1 meal/day: old 1 (Dinner) becomes 3, stale hidden keys are dropped", () => {
const old = { mealsPerDay: 1, slots: { "0-1": STEW, "0-2": SANDWICH } };
expect((migrateV1Slots(old) as MealPlanState).slots).toEqual({ "0-3": STEW });
});
test("3 meals/day: nothing moves", () => {
const old = { mealsPerDay: 3, slots: { "0-1": PANCAKES, "0-3": STEW } };
expect((migrateV1Slots(old) as MealPlanState).slots).toEqual(old.slots);
});
test("migrated plans still pass sanitizeState", () => {
expect(() => sanitizeState(migrateV1Slots({ mealsPerDay: 2, slots: { "0-1": "x" } }))).not.toThrow();
});
});

View file

@ -8,9 +8,7 @@ import { RECIPES_BY_ID } from "./recipes";
//
// Used by:
// - the Server Action (input from the network is untrusted)
// - the localStorage loader (old versions of the app saved older shapes)
//
// Old-shape migrations live here too, so components never see legacy data.
// - the localStorage loader (saved data is untrusted too)
// ---------------------------------------------------------------------------
export const DEFAULT_STATE: MealPlanState = {
@ -26,11 +24,6 @@ export const DEFAULT_STATE: MealPlanState = {
slots: {},
};
/** v1 had a single "asian" cuisine; it was split into four. */
const LEGACY_CUISINES: Record<string, Cuisine[]> = {
asian: ["chinese", "japanese", "korean", "thai"],
};
const VALID_APPLIANCES = new Set<string>(APPLIANCES.map((a) => a.id));
const VALID_CUISINES = new Set<string>(CUISINES.map((c) => c.id));
const VALID_STYLES = new Set<string>(STYLES.map((s) => s.id));
@ -53,17 +46,10 @@ export function sanitizeState(input: unknown): MealPlanState {
if (/^[0-6]-[1-3]$/.test(key) && typeof recipeId === "string" && RECIPES_BY_ID.has(recipeId)) slots[key] = recipeId;
}
// Cuisines, with the legacy "asian" expanded.
const cuisinesIn = Array.isArray(raw.preferredCuisines) ? raw.preferredCuisines.flatMap((c) => LEGACY_CUISINES[c as string] ?? [c]) : [];
// Items. v1 had `ownsPantryStaples: boolean` instead of an on-hand list.
// Items.
const isItem = (id: string) => CATALOG_BY_ID.has(id);
const avoidedItems = onlyValid<ItemId>(raw.avoidedItems, isItem);
const onHandItems = Array.isArray(raw.onHandItems)
? onlyValid<ItemId>(raw.onHandItems, isItem)
: raw.ownsPantryStaples === false
? []
: PANTRY_STAPLE_IDS;
const onHandItems = Array.isArray(raw.onHandItems) ? onlyValid<ItemId>(raw.onHandItems, isItem) : PANTRY_STAPLE_IDS;
return {
targetBudget: clamp(raw.targetBudget, 10, 1000, DEFAULT_STATE.targetBudget),
@ -73,7 +59,7 @@ export function sanitizeState(input: unknown): MealPlanState {
selectedAppliances: Array.isArray(raw.selectedAppliances)
? onlyValid<Appliance>(raw.selectedAppliances, (v) => VALID_APPLIANCES.has(v))
: DEFAULT_STATE.selectedAppliances,
preferredCuisines: onlyValid<Cuisine>(cuisinesIn, (v) => VALID_CUISINES.has(v)),
preferredCuisines: onlyValid<Cuisine>(raw.preferredCuisines, (v) => VALID_CUISINES.has(v)),
preferredStyles: onlyValid<CookingStyle>(raw.preferredStyles, (v) => VALID_STYLES.has(v)),
avoidedItems,
// An item can't be both avoided and on hand; "avoid" wins.
@ -81,26 +67,3 @@ export function sanitizeState(input: unknown): MealPlanState {
slots,
};
}
/**
* Plans saved before meal numbers were fixed counted from the first VISIBLE
* meal: with 2 meals a day, "1" meant Lunch. Today "1" is always Breakfast,
* so shift each meal up by the number of meals that were hidden in front of it.
* 2 meals/day: 1 (Lunch) → 2, 2 (Dinner) → 3
* 1 meal/day: 1 (Dinner) → 3
* Anything that lands past Dinner was already hidden and is dropped.
*/
export function migrateV1Slots(input: unknown): unknown {
if (typeof input !== "object" || input === null) return input;
const raw = input as Record<string, unknown>;
const mealsPerDay = Math.round(clamp(raw.mealsPerDay, 1, 3, DEFAULT_STATE.mealsPerDay));
const offset = 3 - mealsPerDay;
const slots: Record<string, unknown> = {};
for (const [key, recipeId] of Object.entries((raw.slots as Record<string, unknown>) ?? {})) {
const [day, meal] = key.split("-").map(Number);
const fixedMeal = meal + offset;
if (fixedMeal >= 1 && fixedMeal <= 3) slots[`${day}-${fixedMeal}`] = recipeId;
}
return { ...raw, slots };
}

View file

@ -3,7 +3,7 @@
import { useEffect, useReducer, useState } from "react";
import type { Appliance, CookingStyle, Cuisine, ItemId, ItemPreference, MealPlanState } from "@/app/types";
import { toggle, withoutVisibleSlots } from "./plan";
import { DEFAULT_STATE, migrateV1Slots, sanitizeState } from "./sanitize";
import { DEFAULT_STATE, sanitizeState } from "./sanitize";
// ---------------------------------------------------------------------------
// All plan state lives in one reducer, persisted to localStorage.
@ -13,10 +13,7 @@ import { DEFAULT_STATE, migrateV1Slots, sanitizeState } from "./sanitize";
// than a dozen useStates.
// ---------------------------------------------------------------------------
// v2: meal numbers became fixed (1 = Breakfast, 2 = Lunch, 3 = Dinner).
// v1 numbered meals from the first VISIBLE one, so it's converted once on load.
const STORAGE_KEY = "budget-meal-planner:v2";
const LEGACY_STORAGE_KEY = "budget-meal-planner:v1";
export type PlanAction =
| { type: "setBudget"; value: number }
@ -90,12 +87,6 @@ function loadSavedState(): MealPlanState {
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (raw) return sanitizeState(JSON.parse(raw));
const legacy = window.localStorage.getItem(LEGACY_STORAGE_KEY);
if (legacy) {
window.localStorage.removeItem(LEGACY_STORAGE_KEY); // the v2 copy is written on first render
return sanitizeState(migrateV1Slots(JSON.parse(legacy)));
}
return DEFAULT_STATE;
} catch {
return DEFAULT_STATE; // corrupt JSON or blocked storage