feat: shopping list checked file support and comment syntax update#93
feat: shopping list checked file support and comment syntax update#93
Conversation
… lists Implements `.shopping-checked` append-only log support per proposal 0016: - Parse/write `+ name` (checked) and `- name` (unchecked) entries - Case-insensitive matching with last-entry-wins semantics - Compaction against current shopping list to remove stale entries - Change comment syntax from `//` to `--` for line/inline comments - Add `[- ... -]` block comment support
Exposes the shopping_list parse/write APIs and the checked-log state tracking to iOS and Android via UniFFI, so mobile apps can use the same .shopping-list and .shopping-checked file formats as CookCLI.
- Add per-recipe reference checkboxes so users control which sub-recipes get included when adding to the shopping list (all checked by default). - Add menus as a single plan entry with nested recipes (and their sub-refs as grandchildren) instead of one top-level entry per recipe. - Expose name as a derived, read-only field: recipe_display_name(path) is computed on load and on add, so any client-supplied name is no longer silently discarded. - Fix migration bypass: add(), add_menu(), and remove() now call migrate_if_needed() before touching .shopping-list, so the legacy .shopping_list.txt can't be orphaned by a write before the first load. - Fix compact() race: acquire an exclusive advisory file lock (fs2) for the read-modify-write so concurrent check()/uncheck() appends can't be lost. check()/uncheck() also lock while appending. - Ignore .shopping-list, .shopping-checked, and .shopping_list.txt.bak in .gitignore. - Flag the ../cooklang-rs path dependency with a TODO referencing cooklang/cooklang-rs#93 until that PR is released. - Refresh recipe snapshot tests for the new upstream parser output (full unit names, scalable flag).
|
@claude review |
The `path = "../cooklang-rs"` dep only works locally. Point at the upstream branch feat/shopping-list-checked-support until cooklang/cooklang-rs#93 is merged and released.
- Return ShoppingListError::Parse for unterminated block comments instead
of silently swallowing everything after `[-`.
- Replace stripped block comments with a single space so adjacent tokens
don't get glued together (`foo[-x-]bar` -> `foo bar`).
- Normalize whitespace in ingredient/recipe names so the double-space
artifact from block-comment stripping no longer leaks through.
- Clarify `checked_set()` doc comment: the returned set contains
lowercased names only.
- Derive Serialize/Deserialize on CheckEntry to match the rest of the
public types in this module.
- Document parse_checked()'s malformed-line-skipping behavior.
- Simplify the compact_checked() sort closure.
- Sort shopping_checked_set's Vec<String> for deterministic FFI output.
- Fix typo in bindings/Cargo.toml keywords ("unuffi" -> "uniffi").
- Backtick `IndexMap`, `BTreeMap`, `IngredientList` in doc comments. - Replace `or_insert_with(Vec::new)` with `or_default()` in pantry index. - Use `contains_key` instead of `get(...).is_some()` in aisle tests. - Explicit `'_` lifetime on `Vec<Event<'_>>` in parser test helpers. - Normalize `PantryItem::WithAttributes` with no attributes back to `Simple` on parse so write→parse roundtrips preserve the variant.
Previously `compact_checked(entries, &ShoppingList)` walked the list looking for `Ingredient` items. That works only if the caller's list is fully expanded — callers that persist shopping lists as recipe *references* (the on-disk `.shopping-list` format, CookCLI, UniFFI clients) always got an empty ingredient set, so every compaction silently dropped every checked entry. Change the contract to be honest: callers pass an iterable of the ingredient names the user currently sees. That shifts the expansion responsibility back to the layer that has parser context. Makes `collect_ingredient_names` public for callers whose lists happen to contain `Ingredient` items directly. FFI wrapper takes `&[String]`.
commit: |
|
@claude review |
|
Claude finished @dubadub's task in 3m 56s —— View job Code Review
Overall this is a well-designed feature addition. The API surface is clean, tests are thorough, and the docstrings clearly communicate the contracts (especially the Breaking change:
|
0.18.5 landed on crates.io with cooklang/cooklang-rs#93 merged, including the name-based `compact_checked` API plus the typed `ShoppingListError` follow-up. Repoint to the stable version and drop the branch-based git dep — also collapses the duplicate cooklang copies in the workspace (cooklang-find, cooklang-reports, cooklang-language-server all resolve 0.18.5 now), addressing the review concern about multiple cooklang versions compiling.
* feat: migrate shopping list to new format with server-side checked state Replaces the old tab-delimited `.shopping_list.txt` with the new `.shopping-list` format from proposal 0016 and adds `.shopping-checked` for persistent ingredient check state: - Rewrite ShoppingListStore to use cooklang-rs shopping_list parser - Auto-migrate from legacy format on first load (renames old file to .bak) - Add check/uncheck/checked/compact API endpoints - Replace localStorage-based checked state with server-side persistence - Compact checked log on recipe removal to prune stale entries * feat: recipe refs, menu plans, and PR review fixes - Add per-recipe reference checkboxes so users control which sub-recipes get included when adding to the shopping list (all checked by default). - Add menus as a single plan entry with nested recipes (and their sub-refs as grandchildren) instead of one top-level entry per recipe. - Expose name as a derived, read-only field: recipe_display_name(path) is computed on load and on add, so any client-supplied name is no longer silently discarded. - Fix migration bypass: add(), add_menu(), and remove() now call migrate_if_needed() before touching .shopping-list, so the legacy .shopping_list.txt can't be orphaned by a write before the first load. - Fix compact() race: acquire an exclusive advisory file lock (fs2) for the read-modify-write so concurrent check()/uncheck() appends can't be lost. check()/uncheck() also lock while appending. - Ignore .shopping-list, .shopping-checked, and .shopping_list.txt.bak in .gitignore. - Flag the ../cooklang-rs path dependency with a TODO referencing cooklang/cooklang-rs#93 until that PR is released. - Refresh recipe snapshot tests for the new upstream parser output (full unit names, scalable flag). * fix(ci): use git dependency for cooklang so CI can resolve it The `path = "../cooklang-rs"` dep only works locally. Point at the upstream branch feat/shopping-list-checked-support until cooklang/cooklang-rs#93 is merged and released. * fix(shopping-list): stop wiping checked log on recipe removal `store.remove()` was calling `compact()`, which delegated to `cooklang::shopping_list::compact_checked(entries, &ShoppingList)`. That upstream helper only kept entries whose names appeared as `Ingredient` items in the list — but our on-disk `.shopping-list` persists recipe *references* only. Every checked entry was therefore considered stale and dropped, so removing any recipe cleared the whole checked file. Pair with the cooklang-rs contract change: `compact_checked` now accepts the user-visible ingredient names directly. Introduce `aggregate_current_ingredient_names()` which walks the stored list and expands each reference via `extract_ingredients` (honoring `included_references` and per-recipe scale). Both the `remove` and explicit `compact` endpoints feed those names into `store.compact()`. Remove failures in aggregation degrade to a warn log so a recipe removal can't fail on a render-time parse error. Also indent nested sub-recipes under their parent recipe card so Pizza Dough visibly nests under Neapolitan Pizza (border-l accent). * fix(shopping-list): address PR review — XSS, atomic compact, error handling Security (high): - Escape every untrusted interpolation (recipe names, paths, ref names, ingredient names, category names) in shopping_list.html. A recipe file called `<img src=x onerror=alert(1)>.cook` is a legal filename and previously executed script via `innerHTML`. - Move inline `onclick="removeRecipe('${path}')"` and `onchange="toggleItem('${itemId}', ..., '${name}')"` to delegated handlers keyed off `data-*` attributes. Script-attribute context no longer receives user-controlled data at all, so a single-quote break in a path/name can't escape. Correctness: - `aggregate_current_ingredient_names` now propagates parse errors via `with_context(..)?` instead of `let _ =`. On remove, the handler still treats compact as best-effort — aggregation failure warns and skips compaction, so a broken recipe can never wipe the checked log. - `compact()` now stages to `.shopping-checked.tmp` and renames into place. A crash between truncate and write previously left the file zero-length; rename is atomic on POSIX so the original is preserved until the new content is fully durable. Refactor: - Extract `to_multiplier(scale)` — was duplicated three times across `add`, `add_menu` (outer), and `add_menu` (inner). Dependency: - Repoint `cooklang` git dep from the now-merged `feat/shopping-list-checked-support` branch to `main` (picks up 0.18.5-dev with #93 merged plus the typed `ShoppingListError` follow- up). To swap to a crates.io version once the next release lands. * chore(deps): switch cooklang to crates.io 0.18.5 0.18.5 landed on crates.io with cooklang/cooklang-rs#93 merged, including the name-based `compact_checked` API plus the typed `ShoppingListError` follow-up. Repoint to the stable version and drop the branch-based git dep — also collapses the duplicate cooklang copies in the workspace (cooklang-find, cooklang-reports, cooklang-language-server all resolve 0.18.5 now), addressing the review concern about multiple cooklang versions compiling. * fix(shopping-list): serialize checked-log writes with in-process mutex File-level flock doesn't prevent two Axum tasks in the same process from racing on `.shopping-checked` — the kernel treats them as one lock owner, so a concurrent compact could still truncate between an append's open and write. Replace flock with a `tokio::sync::Mutex<()>` on AppState held by every check/uncheck/compact handler (including the post-remove compact). Drops the fs2 dependency and simplifies `compact()` to a plain read + atomic temp-file rename. Also refreshes 5 snapshot fixtures to match cooklang 0.18.5 output. * fix(shopping-list): address PR review — clear race, dup recipes, URL encoding - `clear_shopping_list` now acquires `checked_log_lock` so a concurrent check/uncheck can't recreate `.shopping-checked` between our remove_file and the caller's view of a cleared list. - `aggregate_current_ingredient_names` now resets `seen` per top-level shopping-list entry. The list may legitimately contain the same recipe multiple times (e.g. duplicates from legacy format); sharing one `seen` across all entries misreported the second occurrence as a circular dependency, skipped compaction, and let stale checks accumulate. - Replaced `encodeURI(path)` in shopping_list.html with a segment-wise `encodeRecipePath` helper that uses `encodeURIComponent` per segment. `encodeURI` leaves `#`, `?`, `&`, `=` alone (they're valid URI delimiters), which breaks recipe filenames that contain them. - `compact()` now removes the `.shopping-checked.tmp` file on write or rename failure so crashes don't leave dangling temp files.
Summary
.shopping-checkedappend-only log support per proposal 0016: parse/write+ name(checked) and- name(unchecked) entries with case-insensitive last-entry-wins semantics//to--for line/inline comments, add[- ... -]block comment supportCheckEntry,parse_checked(),checked_set(),write_check_entry(),compact_checked()Test plan
--syntax