Ground new Regulars in the background and add recipe fix-up API
CI / ci (push) Successful in 2m37s
CI / ci (pull_request) Successful in 2m18s

Creating a Regular now enqueues a ground_dinner job and returns at once
with its grounding_job_id. The job grounds the dinner's recipe_url with
GroundURL, else its name with Ground, and sets grounding_status grounded
(recipe, photo, method, minutes, difficulty) or failed with a
grounding_error reason code (migration 0009). It writes only a dinner
still pending, so a fix made meanwhile wins.

GET /api/v1/dinners/{id}/recipe-candidates returns up to 3 ranked
candidates {candidate_id, title, site_name, photo_url, minutes} without
downloading photos or running the model. POST /api/v1/dinners/{id}/ground
takes exactly one of {candidate_id}, {recipe_url} or {manual: {...}} and
sets grounded or manual.

PhotoCache takes an ImageFetcher interface and recipestest provides fakes
so other packages can test grounding with fake search, fetch and model.
main wires the Grounder with normalize_recipe over the AI settings.

Maestro task: 0cbaa5bf-8be4-4cbd-aab2-891e7f1bc2b5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 16:14:31 -04:00
co-authored by Claude Opus 5.5
parent 729c5bd0fe
commit d6d82e8368
22 changed files with 2103 additions and 82 deletions
+61
View File
@@ -10,6 +10,67 @@ import (
"database/sql"
)
const createManualRecipe = `-- name: CreateManualRecipe :one
INSERT INTO recipes (
id, source_url, title, total_minutes, raw_jsonld, method,
minutes_total, fetched_at, created_at, updated_at
) VALUES (
?, ?, ?, ?, ?, ?,
?, ?, ?, ?
)
RETURNING id, source_url, site_name, title, total_minutes, prep_minutes, cook_minutes, yield, raw_jsonld, fetched_at, created_at, updated_at, photo_id, method, difficulty, minutes_total
`
type CreateManualRecipeParams struct {
ID string
SourceUrl string
Title string
TotalMinutes sql.NullInt64
RawJsonld string
Method sql.NullString
MinutesTotal sql.NullInt64
FetchedAt string
CreatedAt string
UpdatedAt string
}
// A recipe the household typed in. Its source_url is a mealquest:manual:
// URN rather than a page, so it never matches a fetched page.
func (q *Queries) CreateManualRecipe(ctx context.Context, arg CreateManualRecipeParams) (Recipe, error) {
row := q.db.QueryRowContext(ctx, createManualRecipe,
arg.ID,
arg.SourceUrl,
arg.Title,
arg.TotalMinutes,
arg.RawJsonld,
arg.Method,
arg.MinutesTotal,
arg.FetchedAt,
arg.CreatedAt,
arg.UpdatedAt,
)
var i Recipe
err := row.Scan(
&i.ID,
&i.SourceUrl,
&i.SiteName,
&i.Title,
&i.TotalMinutes,
&i.PrepMinutes,
&i.CookMinutes,
&i.Yield,
&i.RawJsonld,
&i.FetchedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.PhotoID,
&i.Method,
&i.Difficulty,
&i.MinutesTotal,
)
return i, err
}
const createRecipeIngredient = `-- name: CreateRecipeIngredient :exec
INSERT INTO recipe_ingredients (
id, recipe_id, position, raw_text, name, qty, unit, note,