Maestro task 099e9952-7f98-4700-8369-241c589a1e9d. GET /api/v1/dinners/{id} returns everything the recipe detail screen and the tablet reading mode need in one call: name, status, origin, method, minutes_total, difficulty, is_favorite, the two cached photo sizes (or null), ingredients in recipe order as {name, qty, unit, note, raw_text}, steps in order, source {url, site_name} for grounded recipes, grounding_status, and why_picked — the reason of the suggestion that created the dinner, null when none did. Unknown and archived ids are 404.
This is the shape the next four tasks consume (mock 1k detail, Add to this week and favourite, mock 2a reading mode, Wake Lock), so its report records two things for them explicitly: the detail has no recipe_id/timestamps and only the photo_urls map, and POST /dinners/{id}/favorite still answers with the list-style dinner shape, so the favourite task must refetch or patch the detail cache rather than swap that response in.
Contract the tests pin down: ordering is asserted against storage order deliberately reversed, so a query's natural row order cannot satisfy it; both photo URLs are actually fetched and return 200 image/jpeg; why_picked is null for a dinner no suggestion created; archived and unknown ids are 404 and no session is 401. Three variants are covered as the done-condition requires — a grounded Side quest with a reason (run through the real grounding job), a manual Regular typed in by hand with no photo or source, and a failed-grounding dinner with empty ingredients. A grounded or manual dinner without a recipe returns 500 rather than a pretend detail.
Checks:gofmt -l . empty, go vet ./... clean, go test -race -count=1 ./... all packages ok, CGO_ENABLED=0 go build ./... ok, make sqlc-check no drift. New: internal/dinners/detail.go, internal/server/dinners.go handler, one new sqlc query in migrations/queries/suggestions.sql with regenerated internal/db code, internal/server/dinnerdetail_test.go, and an ARCHITECTURE.md entry; dinners_test.go now serves photos from the same store and folder the grounder writes to, because its old separate empty cache made photo URLs unfetchable.
The developer did not start a real server and call the endpoint by hand; it was exercised through the router and auth middleware in the API tests. Web checks were not run — no frontend in this task.
UPDATE after review remediation (d37a784, same pull request): the contract changed in two ways the next tasks must know. source.url is now returned only when the stored source URL is safe to hand to a client — the raw stored URL is never returned, because a pasted or offered URL can carry a token in its path or query, and when no safe URL can be established the field is absent while source.site_name is still present. And why_picked is now constrained to the suggestion that actually created the dinner rather than the earliest row for it. The read also fails fast, with a clear error, when a recipe has two items at the same position, instead of returning an order nobody can trust; the unenforced position-uniqueness invariant is tracked separately.
## Dinner detail API (Feature 15, task 1)
Maestro task `099e9952-7f98-4700-8369-241c589a1e9d`. `GET /api/v1/dinners/{id}` returns everything the recipe detail screen and the tablet reading mode need in one call: name, status, origin, method, minutes_total, difficulty, is_favorite, the two cached photo sizes (or null), ingredients in recipe order as `{name, qty, unit, note, raw_text}`, steps in order, `source {url, site_name}` for grounded recipes, `grounding_status`, and `why_picked` — the reason of the suggestion that created the dinner, null when none did. Unknown and archived ids are 404.
This is the shape the next four tasks consume (mock 1k detail, Add to this week and favourite, mock 2a reading mode, Wake Lock), so its report records two things for them explicitly: the detail has no `recipe_id`/timestamps and only the `photo_urls` map, and `POST /dinners/{id}/favorite` still answers with the list-style dinner shape, so the favourite task must refetch or patch the detail cache rather than swap that response in.
**Contract the tests pin down:** ordering is asserted against storage order deliberately reversed, so a query's natural row order cannot satisfy it; both photo URLs are actually fetched and return 200 `image/jpeg`; `why_picked` is null for a dinner no suggestion created; archived and unknown ids are 404 and no session is 401. Three variants are covered as the done-condition requires — a grounded Side quest with a reason (run through the real grounding job), a manual Regular typed in by hand with no photo or source, and a failed-grounding dinner with empty ingredients. A grounded or manual dinner without a recipe returns 500 rather than a pretend detail.
**Checks:** `gofmt -l .` empty, `go vet ./...` clean, `go test -race -count=1 ./...` all packages ok, `CGO_ENABLED=0 go build ./...` ok, `make sqlc-check` no drift. New: `internal/dinners/detail.go`, `internal/server/dinners.go` handler, one new sqlc query in `migrations/queries/suggestions.sql` with regenerated `internal/db` code, `internal/server/dinnerdetail_test.go`, and an ARCHITECTURE.md entry; `dinners_test.go` now serves photos from the same store and folder the grounder writes to, because its old separate empty cache made photo URLs unfetchable.
The developer did not start a real server and call the endpoint by hand; it was exercised through the router and auth middleware in the API tests. Web checks were not run — no frontend in this task.
**UPDATE after review remediation (`d37a784`, same pull request):** the contract changed in two ways the next tasks must know. `source.url` is now returned only when the stored source URL is safe to hand to a client — the raw stored URL is never returned, because a pasted or offered URL can carry a token in its path or query, and when no safe URL can be established the field is absent while `source.site_name` is still present. And `why_picked` is now constrained to the suggestion that actually created the dinner rather than the earliest row for it. The read also fails fast, with a clear error, when a recipe has two items at the same position, instead of returning an order nobody can trust; the unenforced position-uniqueness invariant is tracked separately.
Returns a dinner with its recipe for the recipe detail screen and
Kitchen reading mode: photo URLs at both cached sizes (400/1200) or
null, ingredients and steps in recipe position order, the source page
for a fetched recipe, grounding status and error, and why_picked, the
reason of the suggestion that created the dinner (null when none).
Unknown and archived dinners are 404. Read in one transaction.
Adds the GetDinnerSuggestionReason query (sqlc regenerated). The
dinners test env now serves photos from the grounder's own store and
data directory so photo URLs can be fetched in tests.
Maestro task 099e9952-7f98-4700-8369-241c589a1e9d.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read-only review — CHANGES_REQUIRED at 2217f4e5ad383c06a95bd52b2ec36ce7199e0a91
F-01 / HIGH — internal/dinners/detail.go:132 (readRecipe).source.url copies recipes.source_url into the response unchanged, and GroundURL stores a pasted or offered URL as given — including a token in its path or query. TestRecipeCandidatesHideTokenURLs (internal/server/grounding_test.go:284) already establishes a reachable path that stores a URL containing access_token, so this can expose a personal token to the client and breaks the existing dinner-API secrecy rule. Fix by returning a separately verified public source URL (reusing whatever mechanism already hides token URLs in the candidate flow) or omitting the URL when no safe one exists, and updating the contract and tests for the omitted case.
F-02 / HIGH (mandatory gate) — the web checks were not run.AGENTS.md requires cd web && npm run typecheck && npm test && npm run build for every change before review. This one is the orchestrator's fault, not the developer's: the dispatch prompt told the developer "Web checks are not needed — no frontend in this task", which contradicts the guide. The reviewer notes it does not claim the web checks would fail; the gate is simply unrun. The orchestrator will run the full web floor at the remediation head and record it here rather than spend a developer cycle on it.
F-03 / MEDIUM — migrations/queries/suggestions.sql:120 (GetDinnerSuggestionReason). The query treats the earliest row as the suggestion that created the dinner, but suggestions.dinner_id has no unique constraint, so with more than one row it can return a declined or superseded reason. Current writes create a fresh dinner per suggestion (so 1:1 holds in practice) and the new test inserts a single suggestion, bypassing the acceptance flow — hence MEDIUM. Being tightened in remediation so the query's intent is explicit in the data model, with a test for the multi-row case.
F-04 / MEDIUM — internal/dinners/detail.go:135. The recipe-item queries sort by position, id; duplicate positions are allowed by the schema, so the ID tie-breaker cannot recover recipe order. Being fixed per the fail-fast house rule rather than left to luck; the unenforced schema invariant is being recorded as its own follow-up.
Confirmed sound: the remaining contract fields are mapped as requested; archived and unknown dinners return 404; the grounded/manual no-recipe 500 follows fail-fast and is not produced by the inspected write paths; photo paths use the two cached sizes; and the changed test fixture (dinners_test.go) serves photos from the grounder's cache without weakening its existing assertions. The supplied Go and sqlc-check results are clean.
## Read-only review — `CHANGES_REQUIRED` at `2217f4e5ad383c06a95bd52b2ec36ce7199e0a91`
**F-01 / HIGH — `internal/dinners/detail.go:132` (`readRecipe`).** `source.url` copies `recipes.source_url` into the response unchanged, and `GroundURL` stores a pasted or offered URL as given — including a token in its path or query. `TestRecipeCandidatesHideTokenURLs` (`internal/server/grounding_test.go:284`) already establishes a reachable path that stores a URL containing `access_token`, so this can expose a personal token to the client and breaks the existing dinner-API secrecy rule. Fix by returning a separately verified public source URL (reusing whatever mechanism already hides token URLs in the candidate flow) or omitting the URL when no safe one exists, and updating the contract and tests for the omitted case.
**F-02 / HIGH (mandatory gate) — the web checks were not run.** `AGENTS.md` requires `cd web && npm run typecheck && npm test && npm run build` for every change before review. **This one is the orchestrator's fault, not the developer's:** the dispatch prompt told the developer "Web checks are not needed — no frontend in this task", which contradicts the guide. The reviewer notes it does *not* claim the web checks would fail; the gate is simply unrun. The orchestrator will run the full web floor at the remediation head and record it here rather than spend a developer cycle on it.
**F-03 / MEDIUM — `migrations/queries/suggestions.sql:120` (`GetDinnerSuggestionReason`).** The query treats the earliest row as the suggestion that created the dinner, but `suggestions.dinner_id` has no unique constraint, so with more than one row it can return a declined or superseded reason. Current writes create a fresh dinner per suggestion (so 1:1 holds in practice) and the new test inserts a single suggestion, bypassing the acceptance flow — hence MEDIUM. Being tightened in remediation so the query's intent is explicit in the data model, with a test for the multi-row case.
**F-04 / MEDIUM — `internal/dinners/detail.go:135`.** The recipe-item queries sort by `position, id`; duplicate positions are allowed by the schema, so the ID tie-breaker cannot recover recipe order. Being fixed per the fail-fast house rule rather than left to luck; the unenforced schema invariant is being recorded as its own follow-up.
**Confirmed sound:** the remaining contract fields are mapped as requested; archived and unknown dinners return 404; the grounded/manual no-recipe 500 follows fail-fast and is not produced by the inspected write paths; photo paths use the two cached sizes; and the changed test fixture (`dinners_test.go`) serves photos from the grounder's cache without weakening its existing assertions. The supplied Go and `sqlc-check` results are clean.
Dinner detail review fixes for GET /api/v1/dinners/{id}:
- source is now {site_name} with no url. A stored source_url may be a
pasted or offered link carrying a personal token, and nothing can show
a stored link is free of one; the candidate flow keeps every page URL
on the server for the same reason, so the detail does too.
- why_picked comes from GetDinnerSuggestionReasons, which reads only
pending or accepted suggestions: a suggestion is only ever created with
a new dinner and declining one archives that dinner, so a declined row
never created a live dinner. More than one candidate row is an error
rather than a guess. sqlc regenerated.
- Two recipe ingredients or steps at one position leave recipe order
unknown, so the read fails rather than ordering them by id.
Maestro task 099e9952-7f98-4700-8369-241c589a1e9d.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A page that names no site is stored under its host, which may be the
pasted link's (a token in a subdomain) or a redirect's. The dinner
detail now returns source.site_name only when the stored name could not
be a host: the link's own host, an IP address or a dotted host name is
withheld as null, so "Recipe from {site}" shows only real attribution.
The extractor's host fallback is shared as recipes.HostSiteName.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Dinner detail API (Feature 15, task 1)
Maestro task
099e9952-7f98-4700-8369-241c589a1e9d.GET /api/v1/dinners/{id}returns everything the recipe detail screen and the tablet reading mode need in one call: name, status, origin, method, minutes_total, difficulty, is_favorite, the two cached photo sizes (or null), ingredients in recipe order as{name, qty, unit, note, raw_text}, steps in order,source {url, site_name}for grounded recipes,grounding_status, andwhy_picked— the reason of the suggestion that created the dinner, null when none did. Unknown and archived ids are 404.This is the shape the next four tasks consume (mock 1k detail, Add to this week and favourite, mock 2a reading mode, Wake Lock), so its report records two things for them explicitly: the detail has no
recipe_id/timestamps and only thephoto_urlsmap, andPOST /dinners/{id}/favoritestill answers with the list-style dinner shape, so the favourite task must refetch or patch the detail cache rather than swap that response in.Contract the tests pin down: ordering is asserted against storage order deliberately reversed, so a query's natural row order cannot satisfy it; both photo URLs are actually fetched and return 200
image/jpeg;why_pickedis null for a dinner no suggestion created; archived and unknown ids are 404 and no session is 401. Three variants are covered as the done-condition requires — a grounded Side quest with a reason (run through the real grounding job), a manual Regular typed in by hand with no photo or source, and a failed-grounding dinner with empty ingredients. A grounded or manual dinner without a recipe returns 500 rather than a pretend detail.Checks:
gofmt -l .empty,go vet ./...clean,go test -race -count=1 ./...all packages ok,CGO_ENABLED=0 go build ./...ok,make sqlc-checkno drift. New:internal/dinners/detail.go,internal/server/dinners.gohandler, one new sqlc query inmigrations/queries/suggestions.sqlwith regeneratedinternal/dbcode,internal/server/dinnerdetail_test.go, and an ARCHITECTURE.md entry;dinners_test.gonow serves photos from the same store and folder the grounder writes to, because its old separate empty cache made photo URLs unfetchable.The developer did not start a real server and call the endpoint by hand; it was exercised through the router and auth middleware in the API tests. Web checks were not run — no frontend in this task.
UPDATE after review remediation (
d37a784, same pull request): the contract changed in two ways the next tasks must know.source.urlis now returned only when the stored source URL is safe to hand to a client — the raw stored URL is never returned, because a pasted or offered URL can carry a token in its path or query, and when no safe URL can be established the field is absent whilesource.site_nameis still present. Andwhy_pickedis now constrained to the suggestion that actually created the dinner rather than the earliest row for it. The read also fails fast, with a clear error, when a recipe has two items at the same position, instead of returning an order nobody can trust; the unenforced position-uniqueness invariant is tracked separately.Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.
Read-only review —
CHANGES_REQUIREDat2217f4e5ad383c06a95bd52b2ec36ce7199e0a91F-01 / HIGH —
internal/dinners/detail.go:132(readRecipe).source.urlcopiesrecipes.source_urlinto the response unchanged, andGroundURLstores a pasted or offered URL as given — including a token in its path or query.TestRecipeCandidatesHideTokenURLs(internal/server/grounding_test.go:284) already establishes a reachable path that stores a URL containingaccess_token, so this can expose a personal token to the client and breaks the existing dinner-API secrecy rule. Fix by returning a separately verified public source URL (reusing whatever mechanism already hides token URLs in the candidate flow) or omitting the URL when no safe one exists, and updating the contract and tests for the omitted case.F-02 / HIGH (mandatory gate) — the web checks were not run.
AGENTS.mdrequirescd web && npm run typecheck && npm test && npm run buildfor every change before review. This one is the orchestrator's fault, not the developer's: the dispatch prompt told the developer "Web checks are not needed — no frontend in this task", which contradicts the guide. The reviewer notes it does not claim the web checks would fail; the gate is simply unrun. The orchestrator will run the full web floor at the remediation head and record it here rather than spend a developer cycle on it.F-03 / MEDIUM —
migrations/queries/suggestions.sql:120(GetDinnerSuggestionReason). The query treats the earliest row as the suggestion that created the dinner, butsuggestions.dinner_idhas no unique constraint, so with more than one row it can return a declined or superseded reason. Current writes create a fresh dinner per suggestion (so 1:1 holds in practice) and the new test inserts a single suggestion, bypassing the acceptance flow — hence MEDIUM. Being tightened in remediation so the query's intent is explicit in the data model, with a test for the multi-row case.F-04 / MEDIUM —
internal/dinners/detail.go:135. The recipe-item queries sort byposition, id; duplicate positions are allowed by the schema, so the ID tie-breaker cannot recover recipe order. Being fixed per the fail-fast house rule rather than left to luck; the unenforced schema invariant is being recorded as its own follow-up.Confirmed sound: the remaining contract fields are mapped as requested; archived and unknown dinners return 404; the grounded/manual no-recipe 500 follows fail-fast and is not produced by the inspected write paths; photo paths use the two cached sizes; and the changed test fixture (
dinners_test.go) serves photos from the grounder's cache without weakening its existing assertions. The supplied Go andsqlc-checkresults are clean.Dinner detail review fixes for GET /api/v1/dinners/{id}: - source is now {site_name} with no url. A stored source_url may be a pasted or offered link carrying a personal token, and nothing can show a stored link is free of one; the candidate flow keeps every page URL on the server for the same reason, so the detail does too. - why_picked comes from GetDinnerSuggestionReasons, which reads only pending or accepted suggestions: a suggestion is only ever created with a new dinner and declining one archives that dinner, so a declined row never created a live dinner. More than one candidate row is an error rather than a guess. sqlc regenerated. - Two recipe ingredients or steps at one position leave recipe order unknown, so the read fails rather than ordering them by id. Maestro task 099e9952-7f98-4700-8369-241c589a1e9d. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.
A page that names no site is stored under its host, which may be the pasted link's (a token in a subdomain) or a redirect's. The dinner detail now returns source.site_name only when the stored name could not be a host: the link's own host, an IP address or a dotted host name is withheld as null, so "Recipe from {site}" shows only real attribution. The extractor's host fallback is shared as recipes.HostSiteName. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Independent Codex gpt-6-sol review: APPROVED for this exact commit.