Vibecode MealWise
track this build3 phases, 9 steps, beginner friendly0%A useful personal version is absolutely one-shottable: a small local app can turn a typed meal request, ingredients, budget, servings and time into a structured recipe. What does not arrive in one session is MealWise's native iPhone finish, voice input, authenticated quota enforcement, optional cross-device sync, and the ongoing work of keeping AI output reliable and safe to use.
You are building a lean indie version of MealWise. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # MealWise · indie build A one-page recipe generator whose hard problems are refusing to render a half-parsed model response, doing the cost arithmetic itself from prices you maintain, and taking every safety-relevant number from a published chart rather than from generated text. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript, mobile first, one page | the whole product is a box you type into and a recipe you read | | Model | One call with a strict schema, the key held server side in a route handler | the key never reaches the browser and the response never renders unvalidated | | Storage | IndexedDB in the browser for saved recipes, with an export to file | nothing about a weeknight dinner needs an account | | Safety data | A checked-in table of cooking temperatures with its source URL and copy date | this is the one part of the output that must not be generated | | Arithmetic | Costs computed in the application from unit prices you entered | a total the model produced cannot be checked against anything | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and its tests. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An API key for a text model with structured output** · pay per request; a few recipes a week is negligible - Why: The recipe itself is one call, and structured output is what makes the costing and safety matching possible at all. - Get it: Create a key with your chosen provider and set a spend limit in their console. - Verify: One request returns a response that validates against the schema - [ ] **Your food safety authority's current temperature chart** · free - Why: This is the one part of the output that must not be generated, and copying it yourself with the date is the whole safeguard. - Get it: Open the chart published by the authority for your country, copy the figures into the table file, and record the URL and today's date in it. - Verify: The table file contains the figures, the URL and the copy date, and the build fails without it - [ ] **Unit prices for twenty things you actually buy** · free - Why: The cost panel is arithmetic over this table, and with an empty table every recipe is entirely unpriced, which is honest but useless. - Get it: Write down the price and unit for the staples in your kitchen from a recent receipt. - Verify: A generated recipe prices most of its ingredients and counts the rest ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Live grocery prices. - Nutrition figures. - Voice input and a native application. - Accounts, quotas and cloud sync. - native iPhone interaction and voice dictation - server-side authentication, quota enforcement and abuse protection - optional cross-device sync - recipe refinement and regeneration polish - ongoing AI-quality, food-safety and operational maintenance If one of those is essential to you, that is the reason to keep paying for MealWise, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · MealWise The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a recipe generator that costs a meal from prices you supplied and takes its cooking temperatures from a published chart to replace MealWise. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things in a recipe app can hurt somebody: an allergen and an undercooked piece of meat. Neither is allowed to come from the model. Cooking temperatures are read from a table checked into the repository, copied by you from your national food safety authority on the day you build, with the URL and the date stored in the file itself. In the United States that is the safe minimum internal temperature chart published by the Food Safety and Inspection Service; in the United Kingdom the Food Standards Agency publishes its own guidance and it is not phrased the same way. Copy the current figures yourself rather than trusting any number you read in a prompt, including this one. If a generated step mentions a temperature the table does not confirm, the step is flagged and the table's figure is shown beside it. If the table file is missing, the build fails rather than quietly omitting it. Allergens: the app highlights ingredients matching a list the user typed, and it never states that a recipe is safe for anyone. 'Contains none of the ingredients you listed' is a claim about a string match. 'Safe' is a claim about a dish nobody has cooked yet, generated from a sentence. And the costs are estimates from prices you entered, not a price feed. Say so wherever they appear. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 and TypeScript, mobile first, one page · the whole product is a box you type into and a recipe you read - Model: One call with a strict schema, the key held server side in a route handler · the key never reaches the browser and the response never renders unvalidated - Storage: IndexedDB in the browser for saved recipes, with an export to file · nothing about a weeknight dinner needs an account - Safety data: A checked-in table of cooking temperatures with its source URL and copy date · this is the one part of the output that must not be generated - Arithmetic: Costs computed in the application from unit prices you entered · a total the model produced cannot be checked against anything ### Data model (create this before Phase 1) - recipes(id, created_at, request_text, model_id, prompt_version, schema_version, raw_response) - ingredients(recipe_id, ordinal, name, quantity, unit, matched_price_key, unit_price_minor, est_cost_minor) - steps(recipe_id, ordinal, text, temp_key, flagged) · temp_key points into the safety table or is null - safety_temps(key, food, celsius, fahrenheit, rest_minutes, source_url, copied_on) · checked into the repository, never generated - pantry_prices(key, name, unit, unit_price_minor, updated_at) · your prices, because the model does not know what a tin costs where you shop - flags(recipe_id, kind, detail) · kind is allergen_match, unconfirmed_temperature or missing_price - There is no field anywhere holding a claim that a recipe is safe for a person, and no field holding a cost the model produced. ### Phase 1 · One request, one schema, no half-parse Build: One mobile-first page with a request box accepting ingredients on hand, budget, servings, exclusions, time and equipment. Send it with a strict schema demanding quantities and units for every ingredient. Validate the response, retry once on a parse failure, then show the failure with the raw text and a retry rather than rendering a partial recipe. Trap: Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. Done when: A response that does not validate against the schema renders no recipe, tested with a fake returning prose. Every ingredient in a rendered recipe has a quantity and a unit, enforced by the schema rather than by the interface. The model identifier, prompt version and raw response are stored with each recipe. A request that is refused or cut short by the provider produces a message naming what happened, not a spinner that never ends. Do not build yet: No costing, safety table, saving or export yet. ### Phase 2 · The costs are yours, not the model's Build: Maintain a small table of unit prices for things you buy. Match each generated ingredient to a price by name and unit, converting units where the conversion is unambiguous. Compute line costs, total and cost per person in the application. Show unmatched ingredients as unpriced with a count, and exclude them from the total rather than treating them as free. Trap: Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. Trap: Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. Done when: The displayed total equals the sum of the line costs to the smallest currency unit, tested. An ingredient with no matching price is shown as unpriced, excluded from the total, and counted in a visible figure. Re-rendering a saved recipe with unchanged prices produces exactly the same total, tested. No cost figure anywhere in the interface originates in the model response, verified by review and by a test that nulls every cost field in a fake response. Do not build yet: No safety table, saving or export yet. ### Phase 3 · The safety layer Build: Match steps against the safety table by food and render the published minimum internal temperature and rest time from the table, with its source URL and copy date shown. Flag any step mentioning a temperature the table does not confirm. Highlight ingredients matching the user's own exclusion list and state plainly that this is a text match, not a safety check. Trap: A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. Trap: Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. Done when: A step containing a temperature the table does not confirm is flagged, tested with a crafted response. Every rendered temperature displays the source URL and the date the table was copied. The word safe never appears as an assertion about a recipe or a person, asserted by a test that searches the rendered strings. Deleting the safety table file fails the build rather than producing recipes without temperatures. Do not build yet: No saving, export or spending controls yet. ### Phase 4 · Saving, exporting and the honest README Build: Save accepted recipes to IndexedDB with search and deletion. Export the whole library to one file with a single action, and prompt for it after a configured number of saves. Keep the model key in the route handler, never log request text or recipes, and cap requests per session. Write the README before you consider this finished. Trap: Browser storage is deleted when somebody clears site data and can be evicted by the browser itself when the disk is under pressure, so a library somebody built over a year vanishes without an error message. Offer a one-action export, prompt for it after a stated number of saves, and say plainly in the interface and the README that the saved library is not a backup. Trap: Putting the model key in client code because the fetch is simpler there publishes it to everyone who opens the page, and the bill arrives before the discovery does. Keep every model call behind a route handler, and add a test that fails if the key name appears in the client bundle. Done when: The model key does not appear in the client bundle, asserted by a test that searches the built output. Exporting produces one file containing every saved recipe with its flags and costs, and reimporting into an empty browser profile restores them all. Request text and recipe content appear in no log, verified by review of every logging call. The README states that costs are estimates from your own prices, names the safety table's source and copy date, and says the app is not dietary or medical advice. Do not build yet: Nothing after this. ### Out of scope (and why) - Live grocery prices, which need a retailer data source with a licence and a maintained matching problem, and which would turn every estimate into a promise. - Nutrition figures, which require a food composition database and portion assumptions that a generated recipe cannot supply. - Voice input and a native application, which are the finished mobile workflow the paid product actually sells. - Accounts, quotas and cloud sync, which add a server to keep running for a tool that is one page and a text box. ### README must contain - That all costs are estimates computed from prices you entered, and that there is no price feed - Where the cooking temperature table came from, with the URL and the date it was copied, and how to update it - That allergen highlighting is a text match against your own list and is not a safety check - That the saved library lives in browser storage, is not a backup, and how to export it - That this is not dietary, allergy or medical advice, stated before the setup instructions ===== AGENTS.md ===== # Agent instructions · MealWise indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 and TypeScript, mobile first, one page, One call with a strict schema, the key held server side in a route handler, IndexedDB in the browser for saved recipes, with an export to file, A checked-in table of cooking temperatures with its source URL and copy date, Costs computed in the application from unit prices you entered. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. - Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. - Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. - A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. - Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. ===== BUILD_PLAN.md ===== # Build plan · MealWise A one-page recipe generator whose hard problems are refusing to render a half-parsed model response, doing the cost arithmetic itself from prices you maintain, and taking every safety-relevant number from a published chart rather than from generated text. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One request, one schema, no half-parse A meal request produces a validated structured recipe or a clear failure, never something in between. ### Steps 1. Demand quantities and units in the schema Everything downstream needs a number and a unit. Files: `src/lib/schema.ts`, `src/app/api/recipe/route.ts` 2. Retry once, then fail visibly A partial recipe is worse than no recipe. Files: `src/lib/generate.ts`, `tests/parse.test.ts` 3. Store the raw response with the recipe So an odd recipe can be explained rather than argued about. Files: `src/lib/store.ts` ### Done when - [ ] An unvalidated response renders no recipe - [ ] Every ingredient carries a quantity and a unit - [ ] Model id, prompt version and raw response are stored - [ ] A refused or truncated call produces a named message ### Watch out - Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. ## Phase 2 · The costs are yours, not the model's Every figure in the cost panel is arithmetic the application did over prices you entered. ### Steps 1. Ask for quantities and units only The model does not know what a tin costs where you shop. Files: `src/lib/schema.ts` 2. Compute line costs and the total in the app A number a test can check is a number you can argue with. Files: `src/lib/cost.ts`, `tests/cost.test.ts` 3. Show unpriced ingredients rather than zeroing them A missing price is not a free ingredient. Files: `src/components/CostPanel.tsx` ### Done when - [ ] The total matches the sum of line costs exactly - [ ] Unpriced ingredients are excluded and counted - [ ] An unchanged recipe re-renders to the same total - [ ] No cost figure comes from the model response ### Watch out - Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. - Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. ## Phase 3 · The safety layer Cooking temperatures come from the checked-in table and allergen handling never claims safety. ### Steps 1. Copy the current chart yourself, with URL and date The figures change and a stale table is worse than none. Files: `data/safety-temps.json` 2. Flag steps whose temperature the table does not confirm 'Until the juices run clear' is not a measurement. Files: `src/lib/safety.ts`, `tests/safety.test.ts` 3. Assert that nothing in the interface claims safety A string test is the only way this stays true. Files: `tests/no-safety-claims.test.ts` ### Done when - [ ] Unconfirmed temperatures in steps are flagged - [ ] Every temperature shows its source URL and copy date - [ ] No rendered string asserts a recipe is safe - [ ] A missing safety table fails the build ### Watch out - A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. - Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. ## Not in this build - Live grocery prices. - Nutrition figures. - Voice input and a native application. - Accounts, quotas and cloud sync. ## After v1, if you want it - A shopping list that merges several recipes and subtracts what the pantry table says you have - Recipes you cook often, stored once and re-costed against current prices without another model call - A second safety table for a different country, selected by a setting rather than by editing the file ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Held in the route handler only. A test asserts it is absent from the client bundle. MODEL_API_KEY=sk-...redacted # Required. Stored with every recipe, because a different model writes different steps. MODEL_ID=a text model identifier # Required. The checked-in temperature table. A missing file fails the build. SAFETY_TABLE_PATH=./data/safety-temps.json # Required. Used for every cost figure and for the smallest-unit arithmetic. CURRENCY=GBP # Required. How many saves before the app asks you to export. Browser storage is not a backup. EXPORT_PROMPT_AFTER_SAVES=10 # Required. Cap on model calls from one session, so a stuck retry loop is not a bill. REQUESTS_PER_SESSION=30
You are building a lean indie version of MealWise. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # MealWise · indie build A one-page recipe generator whose hard problems are refusing to render a half-parsed model response, doing the cost arithmetic itself from prices you maintain, and taking every safety-relevant number from a published chart rather than from generated text. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript, mobile first, one page | the whole product is a box you type into and a recipe you read | | Model | One call with a strict schema, the key held server side in a route handler | the key never reaches the browser and the response never renders unvalidated | | Storage | IndexedDB in the browser for saved recipes, with an export to file | nothing about a weeknight dinner needs an account | | Safety data | A checked-in table of cooking temperatures with its source URL and copy date | this is the one part of the output that must not be generated | | Arithmetic | Costs computed in the application from unit prices you entered | a total the model produced cannot be checked against anything | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and its tests. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An API key for a text model with structured output** · pay per request; a few recipes a week is negligible - Why: The recipe itself is one call, and structured output is what makes the costing and safety matching possible at all. - Get it: Create a key with your chosen provider and set a spend limit in their console. - Verify: One request returns a response that validates against the schema - [ ] **Your food safety authority's current temperature chart** · free - Why: This is the one part of the output that must not be generated, and copying it yourself with the date is the whole safeguard. - Get it: Open the chart published by the authority for your country, copy the figures into the table file, and record the URL and today's date in it. - Verify: The table file contains the figures, the URL and the copy date, and the build fails without it - [ ] **Unit prices for twenty things you actually buy** · free - Why: The cost panel is arithmetic over this table, and with an empty table every recipe is entirely unpriced, which is honest but useless. - Get it: Write down the price and unit for the staples in your kitchen from a recent receipt. - Verify: A generated recipe prices most of its ingredients and counts the rest ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Live grocery prices. - Nutrition figures. - Voice input and a native application. - Accounts, quotas and cloud sync. - native iPhone interaction and voice dictation - server-side authentication, quota enforcement and abuse protection - optional cross-device sync - recipe refinement and regeneration polish - ongoing AI-quality, food-safety and operational maintenance If one of those is essential to you, that is the reason to keep paying for MealWise, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · MealWise The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a recipe generator that costs a meal from prices you supplied and takes its cooking temperatures from a published chart to replace MealWise. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things in a recipe app can hurt somebody: an allergen and an undercooked piece of meat. Neither is allowed to come from the model. Cooking temperatures are read from a table checked into the repository, copied by you from your national food safety authority on the day you build, with the URL and the date stored in the file itself. In the United States that is the safe minimum internal temperature chart published by the Food Safety and Inspection Service; in the United Kingdom the Food Standards Agency publishes its own guidance and it is not phrased the same way. Copy the current figures yourself rather than trusting any number you read in a prompt, including this one. If a generated step mentions a temperature the table does not confirm, the step is flagged and the table's figure is shown beside it. If the table file is missing, the build fails rather than quietly omitting it. Allergens: the app highlights ingredients matching a list the user typed, and it never states that a recipe is safe for anyone. 'Contains none of the ingredients you listed' is a claim about a string match. 'Safe' is a claim about a dish nobody has cooked yet, generated from a sentence. And the costs are estimates from prices you entered, not a price feed. Say so wherever they appear. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 and TypeScript, mobile first, one page · the whole product is a box you type into and a recipe you read - Model: One call with a strict schema, the key held server side in a route handler · the key never reaches the browser and the response never renders unvalidated - Storage: IndexedDB in the browser for saved recipes, with an export to file · nothing about a weeknight dinner needs an account - Safety data: A checked-in table of cooking temperatures with its source URL and copy date · this is the one part of the output that must not be generated - Arithmetic: Costs computed in the application from unit prices you entered · a total the model produced cannot be checked against anything ### Data model (create this before Phase 1) - recipes(id, created_at, request_text, model_id, prompt_version, schema_version, raw_response) - ingredients(recipe_id, ordinal, name, quantity, unit, matched_price_key, unit_price_minor, est_cost_minor) - steps(recipe_id, ordinal, text, temp_key, flagged) · temp_key points into the safety table or is null - safety_temps(key, food, celsius, fahrenheit, rest_minutes, source_url, copied_on) · checked into the repository, never generated - pantry_prices(key, name, unit, unit_price_minor, updated_at) · your prices, because the model does not know what a tin costs where you shop - flags(recipe_id, kind, detail) · kind is allergen_match, unconfirmed_temperature or missing_price - There is no field anywhere holding a claim that a recipe is safe for a person, and no field holding a cost the model produced. ### Phase 1 · One request, one schema, no half-parse Build: One mobile-first page with a request box accepting ingredients on hand, budget, servings, exclusions, time and equipment. Send it with a strict schema demanding quantities and units for every ingredient. Validate the response, retry once on a parse failure, then show the failure with the raw text and a retry rather than rendering a partial recipe. Trap: Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. Done when: A response that does not validate against the schema renders no recipe, tested with a fake returning prose. Every ingredient in a rendered recipe has a quantity and a unit, enforced by the schema rather than by the interface. The model identifier, prompt version and raw response are stored with each recipe. A request that is refused or cut short by the provider produces a message naming what happened, not a spinner that never ends. Do not build yet: No costing, safety table, saving or export yet. ### Phase 2 · The costs are yours, not the model's Build: Maintain a small table of unit prices for things you buy. Match each generated ingredient to a price by name and unit, converting units where the conversion is unambiguous. Compute line costs, total and cost per person in the application. Show unmatched ingredients as unpriced with a count, and exclude them from the total rather than treating them as free. Trap: Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. Trap: Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. Done when: The displayed total equals the sum of the line costs to the smallest currency unit, tested. An ingredient with no matching price is shown as unpriced, excluded from the total, and counted in a visible figure. Re-rendering a saved recipe with unchanged prices produces exactly the same total, tested. No cost figure anywhere in the interface originates in the model response, verified by review and by a test that nulls every cost field in a fake response. Do not build yet: No safety table, saving or export yet. ### Phase 3 · The safety layer Build: Match steps against the safety table by food and render the published minimum internal temperature and rest time from the table, with its source URL and copy date shown. Flag any step mentioning a temperature the table does not confirm. Highlight ingredients matching the user's own exclusion list and state plainly that this is a text match, not a safety check. Trap: A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. Trap: Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. Done when: A step containing a temperature the table does not confirm is flagged, tested with a crafted response. Every rendered temperature displays the source URL and the date the table was copied. The word safe never appears as an assertion about a recipe or a person, asserted by a test that searches the rendered strings. Deleting the safety table file fails the build rather than producing recipes without temperatures. Do not build yet: No saving, export or spending controls yet. ### Phase 4 · Saving, exporting and the honest README Build: Save accepted recipes to IndexedDB with search and deletion. Export the whole library to one file with a single action, and prompt for it after a configured number of saves. Keep the model key in the route handler, never log request text or recipes, and cap requests per session. Write the README before you consider this finished. Trap: Browser storage is deleted when somebody clears site data and can be evicted by the browser itself when the disk is under pressure, so a library somebody built over a year vanishes without an error message. Offer a one-action export, prompt for it after a stated number of saves, and say plainly in the interface and the README that the saved library is not a backup. Trap: Putting the model key in client code because the fetch is simpler there publishes it to everyone who opens the page, and the bill arrives before the discovery does. Keep every model call behind a route handler, and add a test that fails if the key name appears in the client bundle. Done when: The model key does not appear in the client bundle, asserted by a test that searches the built output. Exporting produces one file containing every saved recipe with its flags and costs, and reimporting into an empty browser profile restores them all. Request text and recipe content appear in no log, verified by review of every logging call. The README states that costs are estimates from your own prices, names the safety table's source and copy date, and says the app is not dietary or medical advice. Do not build yet: Nothing after this. ### Out of scope (and why) - Live grocery prices, which need a retailer data source with a licence and a maintained matching problem, and which would turn every estimate into a promise. - Nutrition figures, which require a food composition database and portion assumptions that a generated recipe cannot supply. - Voice input and a native application, which are the finished mobile workflow the paid product actually sells. - Accounts, quotas and cloud sync, which add a server to keep running for a tool that is one page and a text box. ### README must contain - That all costs are estimates computed from prices you entered, and that there is no price feed - Where the cooking temperature table came from, with the URL and the date it was copied, and how to update it - That allergen highlighting is a text match against your own list and is not a safety check - That the saved library lives in browser storage, is not a backup, and how to export it - That this is not dietary, allergy or medical advice, stated before the setup instructions ===== AGENTS.md ===== # Agent instructions · MealWise indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 and TypeScript, mobile first, one page, One call with a strict schema, the key held server side in a route handler, IndexedDB in the browser for saved recipes, with an export to file, A checked-in table of cooking temperatures with its source URL and copy date, Costs computed in the application from unit prices you entered. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. - Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. - Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. - A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. - Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. ===== BUILD_PLAN.md ===== # Build plan · MealWise A one-page recipe generator whose hard problems are refusing to render a half-parsed model response, doing the cost arithmetic itself from prices you maintain, and taking every safety-relevant number from a published chart rather than from generated text. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One request, one schema, no half-parse A meal request produces a validated structured recipe or a clear failure, never something in between. ### Steps 1. Demand quantities and units in the schema Everything downstream needs a number and a unit. Files: `src/lib/schema.ts`, `src/app/api/recipe/route.ts` 2. Retry once, then fail visibly A partial recipe is worse than no recipe. Files: `src/lib/generate.ts`, `tests/parse.test.ts` 3. Store the raw response with the recipe So an odd recipe can be explained rather than argued about. Files: `src/lib/store.ts` ### Done when - [ ] An unvalidated response renders no recipe - [ ] Every ingredient carries a quantity and a unit - [ ] Model id, prompt version and raw response are stored - [ ] A refused or truncated call produces a named message ### Watch out - Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. ## Phase 2 · The costs are yours, not the model's Every figure in the cost panel is arithmetic the application did over prices you entered. ### Steps 1. Ask for quantities and units only The model does not know what a tin costs where you shop. Files: `src/lib/schema.ts` 2. Compute line costs and the total in the app A number a test can check is a number you can argue with. Files: `src/lib/cost.ts`, `tests/cost.test.ts` 3. Show unpriced ingredients rather than zeroing them A missing price is not a free ingredient. Files: `src/components/CostPanel.tsx` ### Done when - [ ] The total matches the sum of line costs exactly - [ ] Unpriced ingredients are excluded and counted - [ ] An unchanged recipe re-renders to the same total - [ ] No cost figure comes from the model response ### Watch out - Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. - Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. ## Phase 3 · The safety layer Cooking temperatures come from the checked-in table and allergen handling never claims safety. ### Steps 1. Copy the current chart yourself, with URL and date The figures change and a stale table is worse than none. Files: `data/safety-temps.json` 2. Flag steps whose temperature the table does not confirm 'Until the juices run clear' is not a measurement. Files: `src/lib/safety.ts`, `tests/safety.test.ts` 3. Assert that nothing in the interface claims safety A string test is the only way this stays true. Files: `tests/no-safety-claims.test.ts` ### Done when - [ ] Unconfirmed temperatures in steps are flagged - [ ] Every temperature shows its source URL and copy date - [ ] No rendered string asserts a recipe is safe - [ ] A missing safety table fails the build ### Watch out - A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. - Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. ## Not in this build - Live grocery prices. - Nutrition figures. - Voice input and a native application. - Accounts, quotas and cloud sync. ## After v1, if you want it - A shopping list that merges several recipes and subtracts what the pantry table says you have - Recipes you cook often, stored once and re-costed against current prices without another model call - A second safety table for a different country, selected by a setting rather than by editing the file ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Held in the route handler only. A test asserts it is absent from the client bundle. MODEL_API_KEY=sk-...redacted # Required. Stored with every recipe, because a different model writes different steps. MODEL_ID=a text model identifier # Required. The checked-in temperature table. A missing file fails the build. SAFETY_TABLE_PATH=./data/safety-temps.json # Required. Used for every cost figure and for the smallest-unit arithmetic. CURRENCY=GBP # Required. How many saves before the app asks you to export. Browser storage is not a backup. EXPORT_PROMPT_AFTER_SAVES=10 # Required. Cap on model calls from one session, so a stuck retry loop is not a bill. REQUESTS_PER_SESSION=30
You are building a production product version of MealWise. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # MealWise · product brief ## Problem A useful personal version is absolutely one-shottable: a small local app can turn a typed meal request, ingredients, budget, servings and time into a structured recipe. What does not arrive in one session is MealWise's native iPhone finish, voice input, authenticated quota enforcement, optional cross-device sync, and the ongoing work of keeping AI output reliable and safe to use. ## Product outcome A recipe generator that never renders a half-parsed response, computes every cost from prices you maintain, takes every cooking temperature from a chart you copied yourself, and makes no claim about anyone's safety. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - an OpenAI-compatible API key - a modern browser ## Explicit non-goals for v1 - Live grocery prices. - Nutrition figures. - Voice input and a native application. - Accounts, quotas and cloud sync. - native iPhone interaction and voice dictation - server-side authentication, quota enforcement and abuse protection - optional cross-device sync - recipe refinement and regeneration polish - ongoing AI-quality, food-safety and operational maintenance ## Success criteria - An invalid model response is confirmed to render no recipe - No cost figure is confirmed to originate in the model response - An unconfirmed temperature in a step is confirmed flagged - No rendered string is confirmed to assert a recipe is safe - The model key is confirmed absent from the client bundle ===== BRIEF.md ===== # Build brief · MealWise The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a recipe generator that costs a meal from prices you supplied and takes its cooking temperatures from a published chart to replace MealWise. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things in a recipe app can hurt somebody: an allergen and an undercooked piece of meat. Neither is allowed to come from the model. Cooking temperatures are read from a table checked into the repository, copied by you from your national food safety authority on the day you build, with the URL and the date stored in the file itself. In the United States that is the safe minimum internal temperature chart published by the Food Safety and Inspection Service; in the United Kingdom the Food Standards Agency publishes its own guidance and it is not phrased the same way. Copy the current figures yourself rather than trusting any number you read in a prompt, including this one. If a generated step mentions a temperature the table does not confirm, the step is flagged and the table's figure is shown beside it. If the table file is missing, the build fails rather than quietly omitting it. Allergens: the app highlights ingredients matching a list the user typed, and it never states that a recipe is safe for anyone. 'Contains none of the ingredients you listed' is a claim about a string match. 'Safe' is a claim about a dish nobody has cooked yet, generated from a sentence. And the costs are estimates from prices you entered, not a price feed. Say so wherever they appear. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 and TypeScript, mobile first, one page · the whole product is a box you type into and a recipe you read - Model: One call with a strict schema, the key held server side in a route handler · the key never reaches the browser and the response never renders unvalidated - Storage: IndexedDB in the browser for saved recipes, with an export to file · nothing about a weeknight dinner needs an account - Safety data: A checked-in table of cooking temperatures with its source URL and copy date · this is the one part of the output that must not be generated - Arithmetic: Costs computed in the application from unit prices you entered · a total the model produced cannot be checked against anything ### Data model (create this before Phase 1) - recipes(id, created_at, request_text, model_id, prompt_version, schema_version, raw_response) - ingredients(recipe_id, ordinal, name, quantity, unit, matched_price_key, unit_price_minor, est_cost_minor) - steps(recipe_id, ordinal, text, temp_key, flagged) · temp_key points into the safety table or is null - safety_temps(key, food, celsius, fahrenheit, rest_minutes, source_url, copied_on) · checked into the repository, never generated - pantry_prices(key, name, unit, unit_price_minor, updated_at) · your prices, because the model does not know what a tin costs where you shop - flags(recipe_id, kind, detail) · kind is allergen_match, unconfirmed_temperature or missing_price - There is no field anywhere holding a claim that a recipe is safe for a person, and no field holding a cost the model produced. ### Phase 1 · One request, one schema, no half-parse Build: One mobile-first page with a request box accepting ingredients on hand, budget, servings, exclusions, time and equipment. Send it with a strict schema demanding quantities and units for every ingredient. Validate the response, retry once on a parse failure, then show the failure with the raw text and a retry rather than rendering a partial recipe. Trap: Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. Done when: A response that does not validate against the schema renders no recipe, tested with a fake returning prose. Every ingredient in a rendered recipe has a quantity and a unit, enforced by the schema rather than by the interface. The model identifier, prompt version and raw response are stored with each recipe. A request that is refused or cut short by the provider produces a message naming what happened, not a spinner that never ends. Do not build yet: No costing, safety table, saving or export yet. ### Phase 2 · The costs are yours, not the model's Build: Maintain a small table of unit prices for things you buy. Match each generated ingredient to a price by name and unit, converting units where the conversion is unambiguous. Compute line costs, total and cost per person in the application. Show unmatched ingredients as unpriced with a count, and exclude them from the total rather than treating them as free. Trap: Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. Trap: Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. Done when: The displayed total equals the sum of the line costs to the smallest currency unit, tested. An ingredient with no matching price is shown as unpriced, excluded from the total, and counted in a visible figure. Re-rendering a saved recipe with unchanged prices produces exactly the same total, tested. No cost figure anywhere in the interface originates in the model response, verified by review and by a test that nulls every cost field in a fake response. Do not build yet: No safety table, saving or export yet. ### Phase 3 · The safety layer Build: Match steps against the safety table by food and render the published minimum internal temperature and rest time from the table, with its source URL and copy date shown. Flag any step mentioning a temperature the table does not confirm. Highlight ingredients matching the user's own exclusion list and state plainly that this is a text match, not a safety check. Trap: A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. Trap: Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. Done when: A step containing a temperature the table does not confirm is flagged, tested with a crafted response. Every rendered temperature displays the source URL and the date the table was copied. The word safe never appears as an assertion about a recipe or a person, asserted by a test that searches the rendered strings. Deleting the safety table file fails the build rather than producing recipes without temperatures. Do not build yet: No saving, export or spending controls yet. ### Phase 4 · Saving, exporting and the honest README Build: Save accepted recipes to IndexedDB with search and deletion. Export the whole library to one file with a single action, and prompt for it after a configured number of saves. Keep the model key in the route handler, never log request text or recipes, and cap requests per session. Write the README before you consider this finished. Trap: Browser storage is deleted when somebody clears site data and can be evicted by the browser itself when the disk is under pressure, so a library somebody built over a year vanishes without an error message. Offer a one-action export, prompt for it after a stated number of saves, and say plainly in the interface and the README that the saved library is not a backup. Trap: Putting the model key in client code because the fetch is simpler there publishes it to everyone who opens the page, and the bill arrives before the discovery does. Keep every model call behind a route handler, and add a test that fails if the key name appears in the client bundle. Done when: The model key does not appear in the client bundle, asserted by a test that searches the built output. Exporting produces one file containing every saved recipe with its flags and costs, and reimporting into an empty browser profile restores them all. Request text and recipe content appear in no log, verified by review of every logging call. The README states that costs are estimates from your own prices, names the safety table's source and copy date, and says the app is not dietary or medical advice. Do not build yet: Nothing after this. ### Out of scope (and why) - Live grocery prices, which need a retailer data source with a licence and a maintained matching problem, and which would turn every estimate into a promise. - Nutrition figures, which require a food composition database and portion assumptions that a generated recipe cannot supply. - Voice input and a native application, which are the finished mobile workflow the paid product actually sells. - Accounts, quotas and cloud sync, which add a server to keep running for a tool that is one page and a text box. ### README must contain - That all costs are estimates computed from prices you entered, and that there is no price feed - Where the cooking temperature table came from, with the URL and the date it was copied, and how to update it - That allergen highlighting is a text match against your own list and is not a safety check - That the saved library lives in browser storage, is not a backup, and how to export it - That this is not dietary, allergy or medical advice, stated before the setup instructions ===== ARCHITECTURE.md ===== # Architecture · MealWise ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript, mobile first, one page | the whole product is a box you type into and a recipe you read | | Model | One call with a strict schema, the key held server side in a route handler | the key never reaches the browser and the response never renders unvalidated | | Storage | IndexedDB in the browser for saved recipes, with an export to file | nothing about a weeknight dinner needs an account | | Safety data | A checked-in table of cooking temperatures with its source URL and copy date | this is the one part of the output that must not be generated | | Arithmetic | Costs computed in the application from unit prices you entered | a total the model produced cannot be checked against anything | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Request | The single page, the input and the schema. | Any interface, provided quantities and units are mandatory. | | Generation | The server-side call, validation and the retry. | Any provider, provided a parse failure renders nothing. | | Costing | Price matching, conversion and arithmetic. | Any price source, provided the model is never one of them. | | Safety | The checked-in table, matching and flags. | Any authority's chart, provided the URL and date travel with it. | | Library | Browser storage, search, export and deletion. | Any store, provided a one-action export exists. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `MODEL_API_KEY` · required, secret · Held in the route handler only. A test asserts it is absent from the client bundle. - `MODEL_ID` · required · Stored with every recipe, because a different model writes different steps. - `SAFETY_TABLE_PATH` · required · The checked-in temperature table. A missing file fails the build. - `CURRENCY` · required · Used for every cost figure and for the smallest-unit arithmetic. - `EXPORT_PROMPT_AFTER_SAVES` · required · How many saves before the app asks you to export. Browser storage is not a backup. - `REQUESTS_PER_SESSION` · required · Cap on model calls from one session, so a stuck retry loop is not a bill. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · MealWise product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 and TypeScript, mobile first, one page, One call with a strict schema, the key held server side in a route handler, IndexedDB in the browser for saved recipes, with an export to file, A checked-in table of cooking temperatures with its source URL and copy date, Costs computed in the application from unit prices you entered. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. - Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. - Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. - A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. - Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. - Browser storage is deleted when somebody clears site data and can be evicted by the browser itself when the disk is under pressure, so a library somebody built over a year vanishes without an error message. Offer a one-action export, prompt for it after a stated number of saves, and say plainly in the interface and the README that the saved library is not a backup. - Putting the model key in client code because the fetch is simpler there publishes it to everyone who opens the page, and the bill arrives before the discovery does. Keep every model call behind a route handler, and add a test that fails if the key name appears in the client bundle. ===== MILESTONES.md ===== # Delivery milestones · MealWise Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One request, one schema, no half-parse A meal request produces a validated structured recipe or a clear failure, never something in between. ### Steps 1. Demand quantities and units in the schema Everything downstream needs a number and a unit. Files: `src/lib/schema.ts`, `src/app/api/recipe/route.ts` 2. Retry once, then fail visibly A partial recipe is worse than no recipe. Files: `src/lib/generate.ts`, `tests/parse.test.ts` 3. Store the raw response with the recipe So an odd recipe can be explained rather than argued about. Files: `src/lib/store.ts` ### Done when - [ ] An unvalidated response renders no recipe - [ ] Every ingredient carries a quantity and a unit - [ ] Model id, prompt version and raw response are stored - [ ] A refused or truncated call produces a named message ### Watch out - Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. ## M2 · The costs are yours, not the model's Every figure in the cost panel is arithmetic the application did over prices you entered. ### Steps 1. Ask for quantities and units only The model does not know what a tin costs where you shop. Files: `src/lib/schema.ts` 2. Compute line costs and the total in the app A number a test can check is a number you can argue with. Files: `src/lib/cost.ts`, `tests/cost.test.ts` 3. Show unpriced ingredients rather than zeroing them A missing price is not a free ingredient. Files: `src/components/CostPanel.tsx` ### Done when - [ ] The total matches the sum of line costs exactly - [ ] Unpriced ingredients are excluded and counted - [ ] An unchanged recipe re-renders to the same total - [ ] No cost figure comes from the model response ### Watch out - Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. - Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. ## M3 · The safety layer Cooking temperatures come from the checked-in table and allergen handling never claims safety. ### Steps 1. Copy the current chart yourself, with URL and date The figures change and a stale table is worse than none. Files: `data/safety-temps.json` 2. Flag steps whose temperature the table does not confirm 'Until the juices run clear' is not a measurement. Files: `src/lib/safety.ts`, `tests/safety.test.ts` 3. Assert that nothing in the interface claims safety A string test is the only way this stays true. Files: `tests/no-safety-claims.test.ts` ### Done when - [ ] Unconfirmed temperatures in steps are flagged - [ ] Every temperature shows its source URL and copy date - [ ] No rendered string asserts a recipe is safe - [ ] A missing safety table fails the build ### Watch out - A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. - Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. ## M4 · Saving, exporting and the honest README (production only) Saved recipes can leave the browser, the key stays server side, and the README states what the numbers are. ### Steps 1. Prompt for an export after a stated number of saves Browser storage disappears without warning anybody. Files: `src/lib/export.ts`, `src/components/ExportPrompt.tsx` 2. Keep the key server side and test the bundle A key in the bundle is a key on the internet. Files: `src/app/api/recipe/route.ts`, `tests/no-key-in-bundle.test.ts` 3. Write the README before calling it done The limitations are the product here, not a footnote. Files: `README.md` ### Done when - [ ] The key is absent from the client bundle - [ ] Export and reimport restore every saved recipe - [ ] No log line contains a request or a recipe - [ ] The README names the safety source, its date and the limits ### Watch out - Browser storage is deleted when somebody clears site data and can be evicted by the browser itself when the disk is under pressure, so a library somebody built over a year vanishes without an error message. Offer a one-action export, prompt for it after a stated number of saves, and say plainly in the interface and the README that the saved library is not a backup. - Putting the model key in client code because the fetch is simpler there publishes it to everyone who opens the page, and the bill arrives before the discovery does. Keep every model call behind a route handler, and add a test that fails if the key name appears in the client bundle. ===== OPERATIONS.md ===== # Operations · MealWise ## Backup The export file is the backup, and it is the user's job, which is why the app asks for it rather than assuming. The pantry price table is worth exporting too, since it is the only thing here that took real effort to assemble. ## Restore Import an export into an empty browser profile and confirm the recipe count, the flags and the totals match what was exported. A restore that brings back recipes but not their flags has lost the part that matters. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local single-page app this lives in the suite: schema validation against a fake returning prose, cost arithmetic to the smallest unit, the safety table's presence, and the absence of the key from the built bundle. Run all four before every release. ## Incident checklist The incident is somebody following a step that gave no temperature, or trusting an allergen highlight as a safety check. The flags and the stated source date are what keep both of those visible in the interface rather than depending on the README nobody reads. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] An invalid model response is confirmed to render no recipe - [ ] No cost figure is confirmed to originate in the model response - [ ] An unconfirmed temperature in a step is confirmed flagged - [ ] No rendered string is confirmed to assert a recipe is safe - [ ] The model key is confirmed absent from the client bundle ## Launch constraint Do not market omitted MealWise capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Held in the route handler only. A test asserts it is absent from the client bundle. MODEL_API_KEY=sk-...redacted # Required. Stored with every recipe, because a different model writes different steps. MODEL_ID=a text model identifier # Required. The checked-in temperature table. A missing file fails the build. SAFETY_TABLE_PATH=./data/safety-temps.json # Required. Used for every cost figure and for the smallest-unit arithmetic. CURRENCY=GBP # Required. How many saves before the app asks you to export. Browser storage is not a backup. EXPORT_PROMPT_AFTER_SAVES=10 # Required. Cap on model calls from one session, so a stuck retry loop is not a bill. REQUESTS_PER_SESSION=30
# MealWise · indie build A one-page recipe generator whose hard problems are refusing to render a half-parsed model response, doing the cost arithmetic itself from prices you maintain, and taking every safety-relevant number from a published chart rather than from generated text. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript, mobile first, one page | the whole product is a box you type into and a recipe you read | | Model | One call with a strict schema, the key held server side in a route handler | the key never reaches the browser and the response never renders unvalidated | | Storage | IndexedDB in the browser for saved recipes, with an export to file | nothing about a weeknight dinner needs an account | | Safety data | A checked-in table of cooking temperatures with its source URL and copy date | this is the one part of the output that must not be generated | | Arithmetic | Costs computed in the application from unit prices you entered | a total the model produced cannot be checked against anything | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and its tests. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An API key for a text model with structured output** · pay per request; a few recipes a week is negligible - Why: The recipe itself is one call, and structured output is what makes the costing and safety matching possible at all. - Get it: Create a key with your chosen provider and set a spend limit in their console. - Verify: One request returns a response that validates against the schema - [ ] **Your food safety authority's current temperature chart** · free - Why: This is the one part of the output that must not be generated, and copying it yourself with the date is the whole safeguard. - Get it: Open the chart published by the authority for your country, copy the figures into the table file, and record the URL and today's date in it. - Verify: The table file contains the figures, the URL and the copy date, and the build fails without it - [ ] **Unit prices for twenty things you actually buy** · free - Why: The cost panel is arithmetic over this table, and with an empty table every recipe is entirely unpriced, which is honest but useless. - Get it: Write down the price and unit for the staples in your kitchen from a recent receipt. - Verify: A generated recipe prices most of its ingredients and counts the rest ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Live grocery prices. - Nutrition figures. - Voice input and a native application. - Accounts, quotas and cloud sync. - native iPhone interaction and voice dictation - server-side authentication, quota enforcement and abuse protection - optional cross-device sync - recipe refinement and regeneration polish - ongoing AI-quality, food-safety and operational maintenance If one of those is essential to you, that is the reason to keep paying for MealWise, and the README should say so rather than pretend.
# Build brief · MealWise The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a recipe generator that costs a meal from prices you supplied and takes its cooking temperatures from a published chart to replace MealWise. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things in a recipe app can hurt somebody: an allergen and an undercooked piece of meat. Neither is allowed to come from the model. Cooking temperatures are read from a table checked into the repository, copied by you from your national food safety authority on the day you build, with the URL and the date stored in the file itself. In the United States that is the safe minimum internal temperature chart published by the Food Safety and Inspection Service; in the United Kingdom the Food Standards Agency publishes its own guidance and it is not phrased the same way. Copy the current figures yourself rather than trusting any number you read in a prompt, including this one. If a generated step mentions a temperature the table does not confirm, the step is flagged and the table's figure is shown beside it. If the table file is missing, the build fails rather than quietly omitting it. Allergens: the app highlights ingredients matching a list the user typed, and it never states that a recipe is safe for anyone. 'Contains none of the ingredients you listed' is a claim about a string match. 'Safe' is a claim about a dish nobody has cooked yet, generated from a sentence. And the costs are estimates from prices you entered, not a price feed. Say so wherever they appear. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 and TypeScript, mobile first, one page · the whole product is a box you type into and a recipe you read - Model: One call with a strict schema, the key held server side in a route handler · the key never reaches the browser and the response never renders unvalidated - Storage: IndexedDB in the browser for saved recipes, with an export to file · nothing about a weeknight dinner needs an account - Safety data: A checked-in table of cooking temperatures with its source URL and copy date · this is the one part of the output that must not be generated - Arithmetic: Costs computed in the application from unit prices you entered · a total the model produced cannot be checked against anything ### Data model (create this before Phase 1) - recipes(id, created_at, request_text, model_id, prompt_version, schema_version, raw_response) - ingredients(recipe_id, ordinal, name, quantity, unit, matched_price_key, unit_price_minor, est_cost_minor) - steps(recipe_id, ordinal, text, temp_key, flagged) · temp_key points into the safety table or is null - safety_temps(key, food, celsius, fahrenheit, rest_minutes, source_url, copied_on) · checked into the repository, never generated - pantry_prices(key, name, unit, unit_price_minor, updated_at) · your prices, because the model does not know what a tin costs where you shop - flags(recipe_id, kind, detail) · kind is allergen_match, unconfirmed_temperature or missing_price - There is no field anywhere holding a claim that a recipe is safe for a person, and no field holding a cost the model produced. ### Phase 1 · One request, one schema, no half-parse Build: One mobile-first page with a request box accepting ingredients on hand, budget, servings, exclusions, time and equipment. Send it with a strict schema demanding quantities and units for every ingredient. Validate the response, retry once on a parse failure, then show the failure with the raw text and a retry rather than rendering a partial recipe. Trap: Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. Done when: A response that does not validate against the schema renders no recipe, tested with a fake returning prose. Every ingredient in a rendered recipe has a quantity and a unit, enforced by the schema rather than by the interface. The model identifier, prompt version and raw response are stored with each recipe. A request that is refused or cut short by the provider produces a message naming what happened, not a spinner that never ends. Do not build yet: No costing, safety table, saving or export yet. ### Phase 2 · The costs are yours, not the model's Build: Maintain a small table of unit prices for things you buy. Match each generated ingredient to a price by name and unit, converting units where the conversion is unambiguous. Compute line costs, total and cost per person in the application. Show unmatched ingredients as unpriced with a count, and exclude them from the total rather than treating them as free. Trap: Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. Trap: Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. Done when: The displayed total equals the sum of the line costs to the smallest currency unit, tested. An ingredient with no matching price is shown as unpriced, excluded from the total, and counted in a visible figure. Re-rendering a saved recipe with unchanged prices produces exactly the same total, tested. No cost figure anywhere in the interface originates in the model response, verified by review and by a test that nulls every cost field in a fake response. Do not build yet: No safety table, saving or export yet. ### Phase 3 · The safety layer Build: Match steps against the safety table by food and render the published minimum internal temperature and rest time from the table, with its source URL and copy date shown. Flag any step mentioning a temperature the table does not confirm. Highlight ingredients matching the user's own exclusion list and state plainly that this is a text match, not a safety check. Trap: A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. Trap: Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. Done when: A step containing a temperature the table does not confirm is flagged, tested with a crafted response. Every rendered temperature displays the source URL and the date the table was copied. The word safe never appears as an assertion about a recipe or a person, asserted by a test that searches the rendered strings. Deleting the safety table file fails the build rather than producing recipes without temperatures. Do not build yet: No saving, export or spending controls yet. ### Phase 4 · Saving, exporting and the honest README Build: Save accepted recipes to IndexedDB with search and deletion. Export the whole library to one file with a single action, and prompt for it after a configured number of saves. Keep the model key in the route handler, never log request text or recipes, and cap requests per session. Write the README before you consider this finished. Trap: Browser storage is deleted when somebody clears site data and can be evicted by the browser itself when the disk is under pressure, so a library somebody built over a year vanishes without an error message. Offer a one-action export, prompt for it after a stated number of saves, and say plainly in the interface and the README that the saved library is not a backup. Trap: Putting the model key in client code because the fetch is simpler there publishes it to everyone who opens the page, and the bill arrives before the discovery does. Keep every model call behind a route handler, and add a test that fails if the key name appears in the client bundle. Done when: The model key does not appear in the client bundle, asserted by a test that searches the built output. Exporting produces one file containing every saved recipe with its flags and costs, and reimporting into an empty browser profile restores them all. Request text and recipe content appear in no log, verified by review of every logging call. The README states that costs are estimates from your own prices, names the safety table's source and copy date, and says the app is not dietary or medical advice. Do not build yet: Nothing after this. ### Out of scope (and why) - Live grocery prices, which need a retailer data source with a licence and a maintained matching problem, and which would turn every estimate into a promise. - Nutrition figures, which require a food composition database and portion assumptions that a generated recipe cannot supply. - Voice input and a native application, which are the finished mobile workflow the paid product actually sells. - Accounts, quotas and cloud sync, which add a server to keep running for a tool that is one page and a text box. ### README must contain - That all costs are estimates computed from prices you entered, and that there is no price feed - Where the cooking temperature table came from, with the URL and the date it was copied, and how to update it - That allergen highlighting is a text match against your own list and is not a safety check - That the saved library lives in browser storage, is not a backup, and how to export it - That this is not dietary, allergy or medical advice, stated before the setup instructions
# Agent instructions · MealWise indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 and TypeScript, mobile first, one page, One call with a strict schema, the key held server side in a route handler, IndexedDB in the browser for saved recipes, with an export to file, A checked-in table of cooking temperatures with its source URL and copy date, Costs computed in the application from unit prices you entered. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. - Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. - Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. - A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. - Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety.
# Build plan · MealWise A one-page recipe generator whose hard problems are refusing to render a half-parsed model response, doing the cost arithmetic itself from prices you maintain, and taking every safety-relevant number from a published chart rather than from generated text. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One request, one schema, no half-parse A meal request produces a validated structured recipe or a clear failure, never something in between. ### Steps 1. Demand quantities and units in the schema Everything downstream needs a number and a unit. Files: `src/lib/schema.ts`, `src/app/api/recipe/route.ts` 2. Retry once, then fail visibly A partial recipe is worse than no recipe. Files: `src/lib/generate.ts`, `tests/parse.test.ts` 3. Store the raw response with the recipe So an odd recipe can be explained rather than argued about. Files: `src/lib/store.ts` ### Done when - [ ] An unvalidated response renders no recipe - [ ] Every ingredient carries a quantity and a unit - [ ] Model id, prompt version and raw response are stored - [ ] A refused or truncated call produces a named message ### Watch out - Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. ## Phase 2 · The costs are yours, not the model's Every figure in the cost panel is arithmetic the application did over prices you entered. ### Steps 1. Ask for quantities and units only The model does not know what a tin costs where you shop. Files: `src/lib/schema.ts` 2. Compute line costs and the total in the app A number a test can check is a number you can argue with. Files: `src/lib/cost.ts`, `tests/cost.test.ts` 3. Show unpriced ingredients rather than zeroing them A missing price is not a free ingredient. Files: `src/components/CostPanel.tsx` ### Done when - [ ] The total matches the sum of line costs exactly - [ ] Unpriced ingredients are excluded and counted - [ ] An unchanged recipe re-renders to the same total - [ ] No cost figure comes from the model response ### Watch out - Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. - Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. ## Phase 3 · The safety layer Cooking temperatures come from the checked-in table and allergen handling never claims safety. ### Steps 1. Copy the current chart yourself, with URL and date The figures change and a stale table is worse than none. Files: `data/safety-temps.json` 2. Flag steps whose temperature the table does not confirm 'Until the juices run clear' is not a measurement. Files: `src/lib/safety.ts`, `tests/safety.test.ts` 3. Assert that nothing in the interface claims safety A string test is the only way this stays true. Files: `tests/no-safety-claims.test.ts` ### Done when - [ ] Unconfirmed temperatures in steps are flagged - [ ] Every temperature shows its source URL and copy date - [ ] No rendered string asserts a recipe is safe - [ ] A missing safety table fails the build ### Watch out - A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. - Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. ## Not in this build - Live grocery prices. - Nutrition figures. - Voice input and a native application. - Accounts, quotas and cloud sync. ## After v1, if you want it - A shopping list that merges several recipes and subtracts what the pantry table says you have - Recipes you cook often, stored once and re-costed against current prices without another model call - A second safety table for a different country, selected by a setting rather than by editing the file
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Held in the route handler only. A test asserts it is absent from the client bundle. MODEL_API_KEY=sk-...redacted # Required. Stored with every recipe, because a different model writes different steps. MODEL_ID=a text model identifier # Required. The checked-in temperature table. A missing file fails the build. SAFETY_TABLE_PATH=./data/safety-temps.json # Required. Used for every cost figure and for the smallest-unit arithmetic. CURRENCY=GBP # Required. How many saves before the app asks you to export. Browser storage is not a backup. EXPORT_PROMPT_AFTER_SAVES=10 # Required. Cap on model calls from one session, so a stuck retry loop is not a bill. REQUESTS_PER_SESSION=30
# MealWise · product brief ## Problem A useful personal version is absolutely one-shottable: a small local app can turn a typed meal request, ingredients, budget, servings and time into a structured recipe. What does not arrive in one session is MealWise's native iPhone finish, voice input, authenticated quota enforcement, optional cross-device sync, and the ongoing work of keeping AI output reliable and safe to use. ## Product outcome A recipe generator that never renders a half-parsed response, computes every cost from prices you maintain, takes every cooking temperature from a chart you copied yourself, and makes no claim about anyone's safety. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - an OpenAI-compatible API key - a modern browser ## Explicit non-goals for v1 - Live grocery prices. - Nutrition figures. - Voice input and a native application. - Accounts, quotas and cloud sync. - native iPhone interaction and voice dictation - server-side authentication, quota enforcement and abuse protection - optional cross-device sync - recipe refinement and regeneration polish - ongoing AI-quality, food-safety and operational maintenance ## Success criteria - An invalid model response is confirmed to render no recipe - No cost figure is confirmed to originate in the model response - An unconfirmed temperature in a step is confirmed flagged - No rendered string is confirmed to assert a recipe is safe - The model key is confirmed absent from the client bundle
# Build brief · MealWise The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a recipe generator that costs a meal from prices you supplied and takes its cooking temperatures from a published chart to replace MealWise. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things in a recipe app can hurt somebody: an allergen and an undercooked piece of meat. Neither is allowed to come from the model. Cooking temperatures are read from a table checked into the repository, copied by you from your national food safety authority on the day you build, with the URL and the date stored in the file itself. In the United States that is the safe minimum internal temperature chart published by the Food Safety and Inspection Service; in the United Kingdom the Food Standards Agency publishes its own guidance and it is not phrased the same way. Copy the current figures yourself rather than trusting any number you read in a prompt, including this one. If a generated step mentions a temperature the table does not confirm, the step is flagged and the table's figure is shown beside it. If the table file is missing, the build fails rather than quietly omitting it. Allergens: the app highlights ingredients matching a list the user typed, and it never states that a recipe is safe for anyone. 'Contains none of the ingredients you listed' is a claim about a string match. 'Safe' is a claim about a dish nobody has cooked yet, generated from a sentence. And the costs are estimates from prices you entered, not a price feed. Say so wherever they appear. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 and TypeScript, mobile first, one page · the whole product is a box you type into and a recipe you read - Model: One call with a strict schema, the key held server side in a route handler · the key never reaches the browser and the response never renders unvalidated - Storage: IndexedDB in the browser for saved recipes, with an export to file · nothing about a weeknight dinner needs an account - Safety data: A checked-in table of cooking temperatures with its source URL and copy date · this is the one part of the output that must not be generated - Arithmetic: Costs computed in the application from unit prices you entered · a total the model produced cannot be checked against anything ### Data model (create this before Phase 1) - recipes(id, created_at, request_text, model_id, prompt_version, schema_version, raw_response) - ingredients(recipe_id, ordinal, name, quantity, unit, matched_price_key, unit_price_minor, est_cost_minor) - steps(recipe_id, ordinal, text, temp_key, flagged) · temp_key points into the safety table or is null - safety_temps(key, food, celsius, fahrenheit, rest_minutes, source_url, copied_on) · checked into the repository, never generated - pantry_prices(key, name, unit, unit_price_minor, updated_at) · your prices, because the model does not know what a tin costs where you shop - flags(recipe_id, kind, detail) · kind is allergen_match, unconfirmed_temperature or missing_price - There is no field anywhere holding a claim that a recipe is safe for a person, and no field holding a cost the model produced. ### Phase 1 · One request, one schema, no half-parse Build: One mobile-first page with a request box accepting ingredients on hand, budget, servings, exclusions, time and equipment. Send it with a strict schema demanding quantities and units for every ingredient. Validate the response, retry once on a parse failure, then show the failure with the raw text and a retry rather than rendering a partial recipe. Trap: Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. Done when: A response that does not validate against the schema renders no recipe, tested with a fake returning prose. Every ingredient in a rendered recipe has a quantity and a unit, enforced by the schema rather than by the interface. The model identifier, prompt version and raw response are stored with each recipe. A request that is refused or cut short by the provider produces a message naming what happened, not a spinner that never ends. Do not build yet: No costing, safety table, saving or export yet. ### Phase 2 · The costs are yours, not the model's Build: Maintain a small table of unit prices for things you buy. Match each generated ingredient to a price by name and unit, converting units where the conversion is unambiguous. Compute line costs, total and cost per person in the application. Show unmatched ingredients as unpriced with a count, and exclude them from the total rather than treating them as free. Trap: Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. Trap: Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. Done when: The displayed total equals the sum of the line costs to the smallest currency unit, tested. An ingredient with no matching price is shown as unpriced, excluded from the total, and counted in a visible figure. Re-rendering a saved recipe with unchanged prices produces exactly the same total, tested. No cost figure anywhere in the interface originates in the model response, verified by review and by a test that nulls every cost field in a fake response. Do not build yet: No safety table, saving or export yet. ### Phase 3 · The safety layer Build: Match steps against the safety table by food and render the published minimum internal temperature and rest time from the table, with its source URL and copy date shown. Flag any step mentioning a temperature the table does not confirm. Highlight ingredients matching the user's own exclusion list and state plainly that this is a text match, not a safety check. Trap: A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. Trap: Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. Done when: A step containing a temperature the table does not confirm is flagged, tested with a crafted response. Every rendered temperature displays the source URL and the date the table was copied. The word safe never appears as an assertion about a recipe or a person, asserted by a test that searches the rendered strings. Deleting the safety table file fails the build rather than producing recipes without temperatures. Do not build yet: No saving, export or spending controls yet. ### Phase 4 · Saving, exporting and the honest README Build: Save accepted recipes to IndexedDB with search and deletion. Export the whole library to one file with a single action, and prompt for it after a configured number of saves. Keep the model key in the route handler, never log request text or recipes, and cap requests per session. Write the README before you consider this finished. Trap: Browser storage is deleted when somebody clears site data and can be evicted by the browser itself when the disk is under pressure, so a library somebody built over a year vanishes without an error message. Offer a one-action export, prompt for it after a stated number of saves, and say plainly in the interface and the README that the saved library is not a backup. Trap: Putting the model key in client code because the fetch is simpler there publishes it to everyone who opens the page, and the bill arrives before the discovery does. Keep every model call behind a route handler, and add a test that fails if the key name appears in the client bundle. Done when: The model key does not appear in the client bundle, asserted by a test that searches the built output. Exporting produces one file containing every saved recipe with its flags and costs, and reimporting into an empty browser profile restores them all. Request text and recipe content appear in no log, verified by review of every logging call. The README states that costs are estimates from your own prices, names the safety table's source and copy date, and says the app is not dietary or medical advice. Do not build yet: Nothing after this. ### Out of scope (and why) - Live grocery prices, which need a retailer data source with a licence and a maintained matching problem, and which would turn every estimate into a promise. - Nutrition figures, which require a food composition database and portion assumptions that a generated recipe cannot supply. - Voice input and a native application, which are the finished mobile workflow the paid product actually sells. - Accounts, quotas and cloud sync, which add a server to keep running for a tool that is one page and a text box. ### README must contain - That all costs are estimates computed from prices you entered, and that there is no price feed - Where the cooking temperature table came from, with the URL and the date it was copied, and how to update it - That allergen highlighting is a text match against your own list and is not a safety check - That the saved library lives in browser storage, is not a backup, and how to export it - That this is not dietary, allergy or medical advice, stated before the setup instructions
# Architecture · MealWise ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript, mobile first, one page | the whole product is a box you type into and a recipe you read | | Model | One call with a strict schema, the key held server side in a route handler | the key never reaches the browser and the response never renders unvalidated | | Storage | IndexedDB in the browser for saved recipes, with an export to file | nothing about a weeknight dinner needs an account | | Safety data | A checked-in table of cooking temperatures with its source URL and copy date | this is the one part of the output that must not be generated | | Arithmetic | Costs computed in the application from unit prices you entered | a total the model produced cannot be checked against anything | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Request | The single page, the input and the schema. | Any interface, provided quantities and units are mandatory. | | Generation | The server-side call, validation and the retry. | Any provider, provided a parse failure renders nothing. | | Costing | Price matching, conversion and arithmetic. | Any price source, provided the model is never one of them. | | Safety | The checked-in table, matching and flags. | Any authority's chart, provided the URL and date travel with it. | | Library | Browser storage, search, export and deletion. | Any store, provided a one-action export exists. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `MODEL_API_KEY` · required, secret · Held in the route handler only. A test asserts it is absent from the client bundle. - `MODEL_ID` · required · Stored with every recipe, because a different model writes different steps. - `SAFETY_TABLE_PATH` · required · The checked-in temperature table. A missing file fails the build. - `CURRENCY` · required · Used for every cost figure and for the smallest-unit arithmetic. - `EXPORT_PROMPT_AFTER_SAVES` · required · How many saves before the app asks you to export. Browser storage is not a backup. - `REQUESTS_PER_SESSION` · required · Cap on model calls from one session, so a stuck retry loop is not a bill. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · MealWise product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 and TypeScript, mobile first, one page, One call with a strict schema, the key held server side in a route handler, IndexedDB in the browser for saved recipes, with an export to file, A checked-in table of cooking temperatures with its source URL and copy date, Costs computed in the application from unit prices you entered. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. - Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. - Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. - A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. - Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. - Browser storage is deleted when somebody clears site data and can be evicted by the browser itself when the disk is under pressure, so a library somebody built over a year vanishes without an error message. Offer a one-action export, prompt for it after a stated number of saves, and say plainly in the interface and the README that the saved library is not a backup. - Putting the model key in client code because the fetch is simpler there publishes it to everyone who opens the page, and the bill arrives before the discovery does. Keep every model call behind a route handler, and add a test that fails if the key name appears in the client bundle.
# Delivery milestones · MealWise Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One request, one schema, no half-parse A meal request produces a validated structured recipe or a clear failure, never something in between. ### Steps 1. Demand quantities and units in the schema Everything downstream needs a number and a unit. Files: `src/lib/schema.ts`, `src/app/api/recipe/route.ts` 2. Retry once, then fail visibly A partial recipe is worse than no recipe. Files: `src/lib/generate.ts`, `tests/parse.test.ts` 3. Store the raw response with the recipe So an odd recipe can be explained rather than argued about. Files: `src/lib/store.ts` ### Done when - [ ] An unvalidated response renders no recipe - [ ] Every ingredient carries a quantity and a unit - [ ] Model id, prompt version and raw response are stored - [ ] A refused or truncated call produces a named message ### Watch out - Falling back to rendering the model's prose when the structured response fails to parse gives you a recipe that reads perfectly and has no machine-readable quantities, so every cost is missing and no safety step can be matched, and the user cannot tell the difference. Render nothing on a parse failure: show what came back, offer a retry, and keep the failure visible. ## M2 · The costs are yours, not the model's Every figure in the cost panel is arithmetic the application did over prices you entered. ### Steps 1. Ask for quantities and units only The model does not know what a tin costs where you shop. Files: `src/lib/schema.ts` 2. Compute line costs and the total in the app A number a test can check is a number you can argue with. Files: `src/lib/cost.ts`, `tests/cost.test.ts` 3. Show unpriced ingredients rather than zeroing them A missing price is not a free ingredient. Files: `src/components/CostPanel.tsx` ### Done when - [ ] The total matches the sum of line costs exactly - [ ] Unpriced ingredients are excluded and counted - [ ] An unchanged recipe re-renders to the same total - [ ] No cost figure comes from the model response ### Watch out - Letting the model return a cost per ingredient gives you a total that changes between two runs of the same shopping list, cannot be reconciled against a receipt, and is presented with the same confidence as the cooking time. Ask for quantities and units only, look the prices up yourself, and make the arithmetic something a test can check to the penny. - Converting between weight and volume for an ingredient silently assumes a density and produces a price that is wrong by a factor rather than a percentage. Convert only within a measurement family, and leave anything else unpriced with a visible reason. ## M3 · The safety layer Cooking temperatures come from the checked-in table and allergen handling never claims safety. ### Steps 1. Copy the current chart yourself, with URL and date The figures change and a stale table is worse than none. Files: `data/safety-temps.json` 2. Flag steps whose temperature the table does not confirm 'Until the juices run clear' is not a measurement. Files: `src/lib/safety.ts`, `tests/safety.test.ts` 3. Assert that nothing in the interface claims safety A string test is the only way this stays true. Files: `tests/no-safety-claims.test.ts` ### Done when - [ ] Unconfirmed temperatures in steps are flagged - [ ] Every temperature shows its source URL and copy date - [ ] No rendered string asserts a recipe is safe - [ ] A missing safety table fails the build ### Watch out - A generated step saying to cook chicken until the juices run clear is a sentence models write constantly and it is not a temperature; a reader following it has no way to know the difference between that and the step that gives a number. Match the food against your table, show the published figure and rest time beside the step, and flag the vague step rather than letting it stand alone. - Writing that a recipe is safe for someone with an allergy, in any wording, is a claim about a dish nobody has cooked, produced from a sentence, with no knowledge of cross-contamination in the kitchen it will be made in. The interface reports matches against a list the user typed and says exactly that, and there is no string in the codebase asserting safety. ## M4 · Saving, exporting and the honest README (production only) Saved recipes can leave the browser, the key stays server side, and the README states what the numbers are. ### Steps 1. Prompt for an export after a stated number of saves Browser storage disappears without warning anybody. Files: `src/lib/export.ts`, `src/components/ExportPrompt.tsx` 2. Keep the key server side and test the bundle A key in the bundle is a key on the internet. Files: `src/app/api/recipe/route.ts`, `tests/no-key-in-bundle.test.ts` 3. Write the README before calling it done The limitations are the product here, not a footnote. Files: `README.md` ### Done when - [ ] The key is absent from the client bundle - [ ] Export and reimport restore every saved recipe - [ ] No log line contains a request or a recipe - [ ] The README names the safety source, its date and the limits ### Watch out - Browser storage is deleted when somebody clears site data and can be evicted by the browser itself when the disk is under pressure, so a library somebody built over a year vanishes without an error message. Offer a one-action export, prompt for it after a stated number of saves, and say plainly in the interface and the README that the saved library is not a backup. - Putting the model key in client code because the fetch is simpler there publishes it to everyone who opens the page, and the bill arrives before the discovery does. Keep every model call behind a route handler, and add a test that fails if the key name appears in the client bundle.
# Operations · MealWise ## Backup The export file is the backup, and it is the user's job, which is why the app asks for it rather than assuming. The pantry price table is worth exporting too, since it is the only thing here that took real effort to assemble. ## Restore Import an export into an empty browser profile and confirm the recipe count, the flags and the totals match what was exported. A restore that brings back recipes but not their flags has lost the part that matters. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local single-page app this lives in the suite: schema validation against a fake returning prose, cost arithmetic to the smallest unit, the safety table's presence, and the absence of the key from the built bundle. Run all four before every release. ## Incident checklist The incident is somebody following a step that gave no temperature, or trusting an allergen highlight as a safety check. The flags and the stated source date are what keep both of those visible in the interface rather than depending on the README nobody reads. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] An invalid model response is confirmed to render no recipe - [ ] No cost figure is confirmed to originate in the model response - [ ] An unconfirmed temperature in a step is confirmed flagged - [ ] No rendered string is confirmed to assert a recipe is safe - [ ] The model key is confirmed absent from the client bundle ## Launch constraint Do not market omitted MealWise capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Held in the route handler only. A test asserts it is absent from the client bundle. MODEL_API_KEY=sk-...redacted # Required. Stored with every recipe, because a different model writes different steps. MODEL_ID=a text model identifier # Required. The checked-in temperature table. A missing file fails the build. SAFETY_TABLE_PATH=./data/safety-temps.json # Required. Used for every cost figure and for the smallest-unit arithmetic. CURRENCY=GBP # Required. How many saves before the app asks you to export. Browser storage is not a backup. EXPORT_PROMPT_AFTER_SAVES=10 # Required. Cap on model calls from one session, so a stuck retry loop is not a bill. REQUESTS_PER_SESSION=30
$ choose a build depth, inspect the files, then open the complete pack in your agent
People pay when they want a recipe generator that already lives on their phone, accepts voice input, remembers their work locally, and handles account, quota and billing plumbing without a personal API key or a weekend of setup. That is mostly execution rather than a structural moat, but it is still the part that decides whether a personal tool gets used every week.
xnative iPhone interaction and voice dictation
xserver-side authentication, quota enforcement and abuse protection
xoptional cross-device sync
xrecipe refinement and regeneration polish
xongoing AI-quality, food-safety and operational maintenance
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
MealWise pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | 15 recipe generations per calendar month; text and voice requests; local recipe library. |
| basic | $3.99 | $3.33 | 80 recipe generations per calendar month plus per-serving nutrition estimates when available. |
| pro | $7.99 | $6.67 | No monthly product quota, subject to fair use and abuse protection; recipe refinement and regeneration; optional cloud sync when publicly activated. |
free tier15 recipe generations per calendar month at launch.
billingMonthly or yearly auto-renewing subscriptions through Apple; cancel in Apple subscription settings.
hidden costsAI generation remains subject to fair-use and abuse controls; Apple displays taxes and any offer eligibility before purchase.
verified 2026-08-15 · source ↗
Is MealWise free?
The free plan includes 15 recipe generations per calendar month, text and voice requests, local saving, and no advertising. Paid is Pro at $7.99/mo (checked 2026-08-15).
Vibecode MealWise
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal MealWise replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does MealWise cost?
MealWise costs about $7.99/month (Pro, checked 2026-08-15), which is $95.88 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing MealWise?
Honestly: native iPhone interaction and voice dictation; server-side authentication, quota enforcement and abuse protection; optional cross-device sync; recipe refinement and regeneration polish; ongoing AI-quality, food-safety and operational maintenance. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to MealWise?
Yes: Mealie (A self-hosted household recipe server with imports and meal planning; you bring both the recipes and the server.) RecipeSage (A free recipe keeper with planning, shopping lists and an AI cooking assistant; less focused on one-shot budget recipes.) The prompt is for when you want it exactly your way.