fix: add recipe ingredients to catalog synchronously with async AI enrichment #32

Merged
psycodepath merged 3 commits from bugfix/31-ingredients-catalog into main 2026-07-11 19:51:59 +00:00
Owner

Fixes the bug where creating a recipe never added its ingredients to the product catalog.

Root cause

Commit 070542d changed the recipe listener from a synchronous catalog write to enqueue-only, moving the actual insert into the grocery:process-taxonomy worker — but nothing ever ran that worker, so pending_taxonomy_items never drained.

Fix (hybrid)

  • Synchronous: RegisterRecipeIngredientsInCatalogListener now saves a minimal CatalogProduct immediately (deduped by stemmed name, resilient per-ingredient), then queues it for enrichment. The catalog is correct the instant a recipe is saved, even if the worker/Gemini is down.
  • Background: TaxonomySuggestor enriches the existing product in place (reusing its id — no duplicates) with category, shelf life, and canonical unit.
  • Runner: new worker service in docker-compose.yml polls grocery:process-taxonomy (interval via WORKER_INTERVAL, default 60s).

Tests & verification

  • New executable Behat feature (features/recipe_ingredients_catalog.feature) driving the real listener + repository + suggestor — 3 scenarios, all passing.
  • Updated unit tests for the listener and suggestor.
  • PHPStan level 8 clean; full PHPUnit suite green (172 tests).

Docs

Updated docs/modules/grocery.md, docs/cli.md, docs/installation.md, and docs/modules/meals.md to reflect the hybrid flow and the new worker service.

Out of scope (follow-ups, noted in #31)

  • Re-enqueue on recipe edit (Recipe::updateDetails emits no event).
  • Semantic ingredient consolidation beyond stemmed-name dedup.
  • Confirm worker tenant coverage (tenants.json, falls back to default).

Closes #31

Fixes the bug where creating a recipe never added its ingredients to the product catalog. ## Root cause Commit `070542d` changed the recipe listener from a synchronous catalog write to enqueue-only, moving the actual insert into the `grocery:process-taxonomy` worker — but nothing ever ran that worker, so `pending_taxonomy_items` never drained. ## Fix (hybrid) - **Synchronous:** `RegisterRecipeIngredientsInCatalogListener` now saves a minimal `CatalogProduct` immediately (deduped by stemmed name, resilient per-ingredient), then queues it for enrichment. The catalog is correct the instant a recipe is saved, even if the worker/Gemini is down. - **Background:** `TaxonomySuggestor` enriches the existing product **in place** (reusing its id — no duplicates) with category, shelf life, and canonical unit. - **Runner:** new `worker` service in `docker-compose.yml` polls `grocery:process-taxonomy` (interval via `WORKER_INTERVAL`, default 60s). ## Tests & verification - New executable Behat feature (`features/recipe_ingredients_catalog.feature`) driving the real listener + repository + suggestor — 3 scenarios, all passing. - Updated unit tests for the listener and suggestor. - PHPStan level 8 clean; full PHPUnit suite green (172 tests). ## Docs Updated `docs/modules/grocery.md`, `docs/cli.md`, `docs/installation.md`, and `docs/modules/meals.md` to reflect the hybrid flow and the new worker service. ## Out of scope (follow-ups, noted in #31) - Re-enqueue on recipe edit (`Recipe::updateDetails` emits no event). - Semantic ingredient consolidation beyond stemmed-name dedup. - Confirm worker tenant coverage (`tenants.json`, falls back to `default`). Closes #31
Ingredients were only enqueued into pending_taxonomy_items and never
reached the product catalog, because nothing ran the
grocery:process-taxonomy worker (regression introduced in 070542d).

Hybrid fix that makes the synchronous step guarantee catalog membership
and treats AI as a pure enhancement layer:

- RegisterRecipeIngredientsInCatalogListener now persists a minimal
  CatalogProduct synchronously on recipe creation (deduped by stemmed
  name, resilient per-ingredient with logging) and queues it for
  enrichment. The catalog is correct instantly, even if the worker or
  Gemini is unavailable.
- TaxonomySuggestor enriches the existing product in place instead of
  creating a new one, reusing its id so the save updates rather than
  duplicates.
- Add a dedicated docker-compose "worker" service that polls
  grocery:process-taxonomy on an interval to drain the queue.

Worker/Gemini downtime now only delays enrichment; ingredients are never
lost or invisible.

Refs #31

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a feature covering the fix from #31, backed by step definitions that
exercise the real pipeline (RegisterRecipeIngredientsInCatalogListener,
SqliteCatalogProductRepository, TaxonomySuggestor) against in-memory SQLite;
only Gemini/settings/category lookup are deterministic in-context doubles.

Scenarios:
- an ingredient is in the catalog immediately on recipe creation
- duplicate / already-catalogued ingredients are not duplicated
- the background worker enriches an existing product in place

Clears the needs-gherkin requirement on #31.

Refs #31

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs: reflect synchronous catalog add + async enrichment worker
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 52s
CI/CD Pipeline / build-and-push (pull_request) Has been skipped
9f0323ece8
Updates docs to match the #31 fix:
- grocery module: listener now saves a minimal product synchronously and
  queues for enrichment; TaxonomySuggestor enriches in place (not create).
- cli reference: worker enriches (not classifies-to-create) and runs
  automatically via the compose worker service.
- installation: document the docker-compose "worker" service (and correct
  the stale LLDAP note).
- meals module: RecipeCreated now catalogs ingredients and queues enrichment.

Refs #31

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
psycodepath deleted branch bugfix/31-ingredients-catalog 2026-07-11 19:51:59 +00:00
Sign in to join this conversation.
No reviewers
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
psycodepath/hrp!32
No description provided.