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>
84 lines
2.3 KiB
SQL
84 lines
2.3 KiB
SQL
-- name: UpsertRecipe :one
|
|
-- A recipe is keyed by source_url: re-fetching the same URL updates the
|
|
-- existing row and keeps its id and created_at, which the returned row
|
|
-- carries. Replace its ingredients and steps in the same transaction with
|
|
-- DeleteRecipeIngredients/DeleteRecipeSteps then the Create queries.
|
|
INSERT INTO recipes (
|
|
id, source_url, site_name, title, total_minutes, prep_minutes,
|
|
cook_minutes, yield, raw_jsonld, photo_id, method, difficulty,
|
|
minutes_total, fetched_at, created_at, updated_at
|
|
) VALUES (
|
|
?, ?, ?, ?, ?, ?,
|
|
?, ?, ?, ?, ?, ?,
|
|
?, ?, ?, ?
|
|
)
|
|
ON CONFLICT (source_url) DO UPDATE SET
|
|
site_name = excluded.site_name,
|
|
title = excluded.title,
|
|
total_minutes = excluded.total_minutes,
|
|
prep_minutes = excluded.prep_minutes,
|
|
cook_minutes = excluded.cook_minutes,
|
|
yield = excluded.yield,
|
|
raw_jsonld = excluded.raw_jsonld,
|
|
photo_id = excluded.photo_id,
|
|
method = excluded.method,
|
|
difficulty = excluded.difficulty,
|
|
minutes_total = excluded.minutes_total,
|
|
fetched_at = excluded.fetched_at,
|
|
updated_at = excluded.updated_at
|
|
RETURNING *;
|
|
|
|
-- name: GetRecipe :one
|
|
SELECT * FROM recipes
|
|
WHERE id = ?;
|
|
|
|
-- name: GetRecipeBySourceURL :one
|
|
SELECT * FROM recipes
|
|
WHERE source_url = ?;
|
|
|
|
-- name: CreateRecipeIngredient :exec
|
|
INSERT INTO recipe_ingredients (
|
|
id, recipe_id, position, raw_text, name, qty, unit, note,
|
|
created_at, updated_at
|
|
) VALUES (
|
|
?, ?, ?, ?, ?, ?, ?, ?,
|
|
?, ?
|
|
);
|
|
|
|
-- name: ListRecipeIngredients :many
|
|
SELECT * FROM recipe_ingredients
|
|
WHERE recipe_id = ?
|
|
ORDER BY position, id;
|
|
|
|
-- name: DeleteRecipeIngredients :exec
|
|
DELETE FROM recipe_ingredients
|
|
WHERE recipe_id = ?;
|
|
|
|
-- name: CreateRecipeStep :exec
|
|
INSERT INTO recipe_steps (
|
|
id, recipe_id, position, text, created_at, updated_at
|
|
) VALUES (
|
|
?, ?, ?, ?, ?, ?
|
|
);
|
|
|
|
-- name: ListRecipeSteps :many
|
|
SELECT * FROM recipe_steps
|
|
WHERE recipe_id = ?
|
|
ORDER BY position, id;
|
|
|
|
-- name: DeleteRecipeSteps :exec
|
|
DELETE FROM recipe_steps
|
|
WHERE recipe_id = ?;
|
|
|
|
-- name: CreateManualRecipe :one
|
|
-- 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.
|
|
INSERT INTO recipes (
|
|
id, source_url, title, total_minutes, raw_jsonld, method,
|
|
minutes_total, fetched_at, created_at, updated_at
|
|
) VALUES (
|
|
?, ?, ?, ?, ?, ?,
|
|
?, ?, ?, ?
|
|
)
|
|
RETURNING *;
|