From 1cba099f404c2cfd815a8a101af9315bf879c42a Mon Sep 17 00:00:00 2001 From: dev_agent Date: Mon, 5 Oct 2026 15:49:20 -0400 Subject: [PATCH] Document grounding selection and pipeline order The architecture of record still described the first stored search result being returned before ranking. It now says ranking decides among stored and new candidates, a stored source_url being scored on its stored recipe without refetching its page. State the exact per-candidate order in the docs and in ground: the photo download and validation (DownloadPhoto) precedes normalize_recipe, and CachePhoto records the photo in the same transaction as the recipe. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 20 ++++++++++++++------ internal/recipes/ground.go | 5 +++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 808d07d..fc277a5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -299,12 +299,20 @@ automatic** (D-021). recorded twice. Files written for a transaction that rolls back are removed by the caller (`RemovePhotoFiles`). - Grounding (`recipes.Grounder`) ties these together. - `Ground(ctx, dishName, query)` searches for the top 5 candidates; if one - is already a stored `source_url`, the first such is returned without - fetching anything. Otherwise it ranks them and tries each ranked recipe - best first: download its photo, run `normalize_recipe`, then store the - photo, recipe, ingredients and steps in **one transaction** (an existing - `source_url` is updated in place). It returns `Grounded{recipe_id, + `Ground(ctx, dishName, query)` searches for the top 5 candidates and ranks + them all, stored and new alike: a candidate whose `source_url` is already + stored is scored on its stored recipe (title, ingredients, steps, times, + photo source) and its page is never refetched; every other candidate is + fetched and extracted. Ranking alone decides between them. It then tries + each ranked candidate best first. A stored one is returned as it is, so a + stored recipe is reused only when it ranks above every new candidate that + grounds. A new one is grounded in this order: (1) `DownloadPhoto` — + the download and validation step of photo caching — (2) + `normalize_recipe`, then (3) one **transaction** that runs `CachePhoto` + (scales and writes the photo files and records the `photos` row) and + stores the recipe, ingredients and steps (an existing `source_url` is + updated in place); files written for a rolled-back transaction are + removed. It returns `Grounded{recipe_id, photo_id, method, minutes_total, difficulty, source_url, site_name}`. A candidate whose photo fails or whose model output is unusable (`llm.ErrBadOutput`) is passed over; when none grounds, the error is a diff --git a/internal/recipes/ground.go b/internal/recipes/ground.go index 59b304e..bb52b71 100644 --- a/internal/recipes/ground.go +++ b/internal/recipes/ground.go @@ -319,6 +319,11 @@ func (g *Grounder) storedRecipe(ctx context.Context, sourceURL string) (storedRe // ground downloads r's photo, normalizes it and stores it as sourceURL's // recipe. A problem with this recipe is returned as reason, so the caller // can try another; err is a failure that is not the recipe's. +// +// The order is fixed: the photo is downloaded and validated first, so a +// recipe without a usable photo never costs a normalize_recipe call; then +// normalize_recipe runs; then save records the photo (CachePhoto) and the +// recipe in one transaction. func (g *Grounder) ground(ctx context.Context, sourceURL string, r Recipe) (gr Grounded, reason, err error) { photo, err := g.photos.DownloadPhoto(ctx, r.ImageURL) if err != nil {