A new recipes page at /recipes lets a cook paste a plain-text recipe… #6

Merged
cmoriarty merged 3 commits from osf/70-172-comparison-arm-single-scale-recipes into compare/single 2026-10-17 07:23:39 -04:00
Owner

What was done

  • A new recipes page at /recipes lets a cook paste a plain-text recipe and have it read, scaled, unit-converted, and added to a shopping list.
  • It reads the pasted recipe as a "Serves N" line followed by one ingredient per line (a quantity and an optional unit), and reports any line it cannot read with its line number and the reason.
  • It scales the recipe to the servings you ask for (leave Servings blank to keep the recipe's own count), rounding amounts to cook-friendly values like fractions and whole grams.
  • It shows each ingredient in either Metric or US units, converting between the two unit systems, and leaves unit-less ingredients such as eggs as they are.
  • "Add to list" builds a shopping list of up to five recipes that adds up the same ingredient in the same unit, keeps the same ingredient in different units as separate lines, and sorts the lines by name.
  • The list does not persist: it travels with the page in the form, so opening /recipes again starts it empty.
  • The page is documented, and the change is named so the release notes pick it up.
  • The unit and browser suites pass with every spec scenario covered, and a judge opened the page in a real browser and confirmed it behaves as the specs say.

How to review

  1. npm test — the unit suite passes (141 tests), covering reading a recipe, scaling, unit conversion, the shopping list, the page, and the /recipes routes.
  2. npm run test:e2e (run npx playwright install chromium once first) — the browser tests pass, driving /recipes in a real chromium.
  3. npm start and open http://localhost:3000/recipes — the form loads with Metric chosen and "The shopping list is empty."
  4. Paste Serves 2 with a few ingredients (say 200 g flour, a pinch of salt, 2 eggs), set Servings to 4, choose US, and press Show recipe — you get 14 oz flour and 4 eggs, with Line 3: a pinch of salt (no quantity) reported beside it.
  5. Press Add to list — the ingredients join a shopping list that adds up matching items by name across up to five recipes.
  6. Read src/pages.ts (the page) and the /recipes routes in src/server.ts first — that is where the form, the scaling and conversion, and the hidden-field list live.

Code review

Reviewed the recipes change (src/recipe.ts, scale.ts, convert.ts, shopping.ts, the /recipes routes in src/server.ts, the page in src/pages.ts, and all seven new test files) against the proposal, the five spec deltas, design.md and tasks.md. Every spec scenario is implemented and tested with the required tags; all output is HTML-escaped, including the hidden list fields (escapeHtml covers quotes, so a name cannot break out of the value attribute); the /recipes routes sit before the GET / branch; conversion factors, ladder thresholds, rounding boundaries (9.96 oz -> 10 oz, 0.04 g -> 0.1 g, 0.05 tsp -> 1/4), the five-recipe cap, and the name/unit merge and sort order all match the specs. One deviation from the design contract was found and fixed: parseQuantity reused the whole-line regex with its trailing (\s(.*))? group, so it returned 2 for '2 eggs' instead of null as design.md and task 1.1 require; it now uses a strict quantity-only regex while parseIngredientLine keeps the whole-line one, and test/recipe.test.ts pins the new behaviour. npx tsc --noEmit is clean and the narrowed vitest run for the touched file passes; openspec validate scale-recipes-and-shopping-list --strict is clean. No generated, cached, secret or scratch files in the commit set; the .osf records and the openspec change are left as-is.

Fixed before the commit

  • minor, src/recipe.ts:57: parseQuantity matched the whole-line QUANTITY regex, whose (?:\s(.*))? tail accepts trailing text, so parseQuantity('2 eggs') returned 2 instead of null, against the design.md / task 1.1 contract that anything other than a whole number, decimal, fraction or mixed number is null. Fixed by giving parseQuantity a strict quantity-only regex (the whole-line regex, renamed INGREDIENT_LINE, is kept for parseIngredientLine) and adding expect(parseQuantity('2 eggs')).toBeNull() to test/recipe.test.ts.

Tests

All 47 scenarios of scale-recipes-and-shopping-list have a tagged test.

The test lint found, in the run's tests on the last check:

Recorded, not decided

  • e2e/recipes.spec.ts:1 B7: no relative import: this file asserts on things it constructed itself, which is the measured 'asserted on my own mock' failure
  • test/recipes-page.test.ts:1 B7: nothing imported here looks like the module under test ('recipes-page')
  • test/recipes-server.test.ts:1 B7: nothing imported here looks like the module under test ('recipes-server')

Braid run

  • Workflow: Braid's OpenSpec flow, change scale-recipes-and-shopping-list (19 steps)
  • Issue: cmoriarty/braid-combined-test#4
  • Model: vLLM · qwen3.8-27b
  • Time: 1 h 12 m from start to this pull request, 1 h 16 m of it in the model
  • Tokens: 8.7M input, 43k output, 156k reasoning, over 135 model calls
  • Pre-flight estimate: fits, p50 197k / p80 286k tokens against a 394k-token step allowance
  • CI: passed its 6 jobs on 941ea25544
  • Run: run_01M54NFR978YG7CDNWYRA1872F

Opened by Braid.

## What was done - A new recipes page at `/recipes` lets a cook paste a plain-text recipe and have it read, scaled, unit-converted, and added to a shopping list. - It reads the pasted recipe as a "Serves N" line followed by one ingredient per line (a quantity and an optional unit), and reports any line it cannot read with its line number and the reason. - It scales the recipe to the servings you ask for (leave Servings blank to keep the recipe's own count), rounding amounts to cook-friendly values like fractions and whole grams. - It shows each ingredient in either Metric or US units, converting between the two unit systems, and leaves unit-less ingredients such as eggs as they are. - "Add to list" builds a shopping list of up to five recipes that adds up the same ingredient in the same unit, keeps the same ingredient in different units as separate lines, and sorts the lines by name. - The list does not persist: it travels with the page in the form, so opening `/recipes` again starts it empty. - The page is documented, and the change is named so the release notes pick it up. - The unit and browser suites pass with every spec scenario covered, and a judge opened the page in a real browser and confirmed it behaves as the specs say. ## How to review 1. `npm test` — the unit suite passes (141 tests), covering reading a recipe, scaling, unit conversion, the shopping list, the page, and the `/recipes` routes. 2. `npm run test:e2e` (run `npx playwright install chromium` once first) — the browser tests pass, driving `/recipes` in a real chromium. 3. `npm start` and open http://localhost:3000/recipes — the form loads with Metric chosen and "The shopping list is empty." 4. Paste `Serves 2` with a few ingredients (say `200 g flour`, `a pinch of salt`, `2 eggs`), set Servings to 4, choose US, and press `Show recipe` — you get `14 oz flour` and `4 eggs`, with `Line 3: a pinch of salt (no quantity)` reported beside it. 5. Press `Add to list` — the ingredients join a shopping list that adds up matching items by name across up to five recipes. 6. Read `src/pages.ts` (the page) and the `/recipes` routes in `src/server.ts` first — that is where the form, the scaling and conversion, and the hidden-field list live. ## Code review Reviewed the recipes change (src/recipe.ts, scale.ts, convert.ts, shopping.ts, the /recipes routes in src/server.ts, the page in src/pages.ts, and all seven new test files) against the proposal, the five spec deltas, design.md and tasks.md. Every spec scenario is implemented and tested with the required tags; all output is HTML-escaped, including the hidden list fields (escapeHtml covers quotes, so a name cannot break out of the value attribute); the /recipes routes sit before the GET /<code> branch; conversion factors, ladder thresholds, rounding boundaries (9.96 oz -> 10 oz, 0.04 g -> 0.1 g, 0.05 tsp -> 1/4), the five-recipe cap, and the name/unit merge and sort order all match the specs. One deviation from the design contract was found and fixed: parseQuantity reused the whole-line regex with its trailing (\s(.*))? group, so it returned 2 for '2 eggs' instead of null as design.md and task 1.1 require; it now uses a strict quantity-only regex while parseIngredientLine keeps the whole-line one, and test/recipe.test.ts pins the new behaviour. npx tsc --noEmit is clean and the narrowed vitest run for the touched file passes; openspec validate scale-recipes-and-shopping-list --strict is clean. No generated, cached, secret or scratch files in the commit set; the .osf records and the openspec change are left as-is. **Fixed before the commit** - minor, `src/recipe.ts:57`: parseQuantity matched the whole-line QUANTITY regex, whose (?:\s(.*))? tail accepts trailing text, so parseQuantity('2 eggs') returned 2 instead of null, against the design.md / task 1.1 contract that anything other than a whole number, decimal, fraction or mixed number is null. Fixed by giving parseQuantity a strict quantity-only regex (the whole-line regex, renamed INGREDIENT_LINE, is kept for parseIngredientLine) and adding expect(parseQuantity('2 eggs')).toBeNull() to test/recipe.test.ts. ## Tests All 47 scenarios of `scale-recipes-and-shopping-list` have a tagged test. The test lint found, in the run's tests on the last check: **Recorded, not decided** - `e2e/recipes.spec.ts:1` B7: no relative import: this file asserts on things it constructed itself, which is the measured 'asserted on my own mock' failure - `test/recipes-page.test.ts:1` B7: nothing imported here looks like the module under test ('recipes-page') - `test/recipes-server.test.ts:1` B7: nothing imported here looks like the module under test ('recipes-server') ## Braid run - **Workflow:** Braid's OpenSpec flow, change `scale-recipes-and-shopping-list` (19 steps) - **Issue:** cmoriarty/braid-combined-test#4 - **Model:** vLLM · qwen3.8-27b - **Time:** 1 h 12 m from start to this pull request, 1 h 16 m of it in the model - **Tokens:** 8.7M input, 43k output, 156k reasoning, over 135 model calls - **Pre-flight estimate:** fits, p50 197k / p80 286k tokens against a 394k-token step allowance - **CI:** passed its 6 jobs on `941ea25544` - **Run:** [run_01M54NFR978YG7CDNWYRA1872F](http://openspecflow.internal.underthere.xyz/run/run_01M54NFR978YG7CDNWYRA1872F) _Opened by Braid._
check: the code of fix-loop round 1, for the project's CI
All checks were successful
CI / Unit tests (pull_request) Successful in 49s
CI / Unit tests (push) Successful in 1m10s
CI / Browser tests (push) Successful in 1m2s
CI / Release notes (push) Successful in 9s
CI / Release notes (pull_request) Successful in 9s
CI / Browser tests (pull_request) Successful in 59s
941ea25544
cmoriarty changed title from Scale a recipe, convert its units, and build a shopping list to A new recipes page at /recipes lets a cook paste a plain-text recipe… 2026-10-17 07:23:11 -04:00
cmoriarty force-pushed osf/70-172-comparison-arm-single-scale-recipes from 941ea25544
All checks were successful
CI / Unit tests (pull_request) Successful in 49s
CI / Unit tests (push) Successful in 1m10s
CI / Browser tests (push) Successful in 1m2s
CI / Release notes (push) Successful in 9s
CI / Release notes (pull_request) Successful in 9s
CI / Browser tests (pull_request) Successful in 59s
to 86a013816c
All checks were successful
CI / Release notes (push) Successful in 10s
CI / Unit tests (push) Successful in 49s
CI / Browser tests (push) Successful in 1m0s
CI / Release notes (pull_request) Successful in 10s
CI / Unit tests (pull_request) Successful in 53s
CI / Browser tests (pull_request) Successful in 1m20s
2026-10-17 07:23:13 -04:00
Compare
cmoriarty merged commit 816fa8161f into compare/single 2026-10-17 07:23:39 -04:00
cmoriarty deleted branch osf/70-172-comparison-arm-single-scale-recipes 2026-10-17 07:23:40 -04:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cmoriarty/braid-combined-test!6
No description provided.