Maestro task 7ac36526-cb51-4dc7-bbad-6ebbee481942 — "Week rollover freeze and archive API" (Feature 17, task 4 of 7).
Done-condition
shopping.Freeze(weekStart) sets status archived and frozen_at for a list whose week has ended (idempotent); archived lists reject item PATCHes with 409. GET /api/v1/shopping/archive?by=week|month|year returns posts newest first grouped by the requested period (month/year headings in the household timezone), each post {weekStart, week label, title (or null), dinner count, item count, on-hand count, dinner names, current}; GET /shopping/{weekStart} serves archived lists read-only. Tests cover freeze idempotency, 409 after freeze, grouping across a year boundary and the counts.
Scope
The freeze service and the archive API. No UI.
Decisions the developer documented — please judge each
Title at freeze (the recorded hand-off): an untitled list is titled first, through the same archive_title skill, scope, payload source and conditional write the title_supply_run job uses — no second mechanism. If titling fails, the freeze still goes ahead, the failure is logged, and the list keeps the week label for good. If a title_supply_run job is in flight, harness.Guard shares the in-flight run rather than making a second model call. Cost: Freeze can now make one model call bounded by the harness timeout.
Week label case:household.WeekLabel gives "Week 41 · Oct 5 – 11" (the mixed-case form the other APIs already return); mock 1m shows "WEEK 41 · OCT 5 – 11", which the developer expects the UI to produce with CSS capitals. Judge whether one label format is right.
Which lists appear: every Supply run up to and including the household's current week; lists generated ahead for future weeks are left out; past weeks not yet frozen still appear.
Grouping: by the week's Monday (a household-local date), so the week of Monday 28 December is December 2026 and year 2026; by=week keys ISO weeks ("2026-W53" with the week label as its heading), months "2026-10"/"October 2026", years "2026".
"Week has ended" means the week's Monday is before the household's current week's Monday in the household zone, with a test that 23:30 Sunday in Los Angeles is refused even though UTC has already reached Monday.
Counts: dinners are the list's groups in day order (a planned dinner with no recipe has no group and is not counted); items are all items; on-hand is items with needed=false.
Other points for review
Freeze idempotency (frozen_at unchanged on a second freeze), explicit errors for a week not ended and a week with no list, the 409 on PATCH after freeze, and the read-only serve.
by accepts exactly week|month|year; anything else 400. The exact post field set.
The new queries (FreezeShoppingList, which only updates while frozen_at IS NULL, and ListShoppingListsThrough) committed and not hand-edited; no migration.
The injectable clock (SetClock via export_test.go, following internal/gcal) — confirm it is test-only and does not change production behaviour.
Scope, test adequacy (mutations reported each caught), maintainability, error handling.
## Task
Maestro task `7ac36526-cb51-4dc7-bbad-6ebbee481942` — "Week rollover freeze and archive API" (Feature 17, task 4 of 7).
## Done-condition
`shopping.Freeze(weekStart)` sets status archived and `frozen_at` for a list whose week has ended (idempotent); archived lists reject item PATCHes with 409. `GET /api/v1/shopping/archive?by=week|month|year` returns posts newest first grouped by the requested period (month/year headings in the household timezone), each post `{weekStart, week label, title (or null), dinner count, item count, on-hand count, dinner names, current}`; `GET /shopping/{weekStart}` serves archived lists read-only. Tests cover freeze idempotency, 409 after freeze, grouping across a year boundary and the counts.
## Scope
The freeze service and the archive API. No UI.
## Decisions the developer documented — please judge each
- **Title at freeze (the recorded hand-off):** an untitled list is titled first, through the same `archive_title` skill, scope, payload source and conditional write the `title_supply_run` job uses — no second mechanism. If titling fails, **the freeze still goes ahead**, the failure is logged, and the list keeps the week label for good. If a `title_supply_run` job is in flight, `harness.Guard` shares the in-flight run rather than making a second model call. Cost: `Freeze` can now make one model call bounded by the harness timeout.
- **Week label case:** `household.WeekLabel` gives "Week 41 · Oct 5 – 11" (the mixed-case form the other APIs already return); mock 1m shows "WEEK 41 · OCT 5 – 11", which the developer expects the UI to produce with CSS capitals. Judge whether one label format is right.
- **Which lists appear:** every Supply run up to and including the household's current week; lists generated ahead for future weeks are left out; past weeks not yet frozen still appear.
- **Grouping:** by the week's Monday (a household-local date), so the week of Monday 28 December is December 2026 and year 2026; `by=week` keys ISO weeks ("2026-W53" with the week label as its heading), months "2026-10"/"October 2026", years "2026".
- **"Week has ended"** means the week's Monday is before the household's current week's Monday in the household zone, with a test that 23:30 Sunday in Los Angeles is refused even though UTC has already reached Monday.
- **Counts:** dinners are the list's groups in day order (a planned dinner with no recipe has no group and is not counted); items are all items; on-hand is items with `needed=false`.
## Other points for review
- `Freeze` idempotency (`frozen_at` unchanged on a second freeze), explicit errors for a week not ended and a week with no list, the 409 on PATCH after freeze, and the read-only serve.
- `by` accepts exactly week|month|year; anything else 400. The exact post field set.
- The new queries (`FreezeShoppingList`, which only updates while `frozen_at IS NULL`, and `ListShoppingListsThrough`) committed and not hand-edited; no migration.
- The injectable clock (`SetClock` via `export_test.go`, following `internal/gcal`) — confirm it is test-only and does not change production behaviour.
- Scope, test adequacy (mutations reported each caught), maintainability, error handling.
## Evidence (orchestrator-run gate at 6da5643f493a901c65d0c4544c20939a3f3d9f2c)
- `gofmt -l .` — empty; `go vet ./...` — clean
- `go test -race -count=1 ./...` — 27 packages ok, 0 failures (shopping 12.0s, server 187.4s)
- `CGO_ENABLED=0 go build ./...` — ok
- `make sqlc-check SQLC=$(go env GOPATH)/bin/sqlc` — no drift
- `cd web && npm run typecheck && npm test && npm run build` — PASS (no web change)
Implementation commit: `6da5643f493a901c65d0c4544c20939a3f3d9f2c`
shopping.Freeze(weekStart) archives a week's Supply run once its week has
ended in the household's time zone, setting status archived and frozen_at;
it is idempotent, and a week not yet ended (ErrWeekNotEnded) or with no
list (ErrListNotFound) is an explicit error. A list still untitled is
titled first with archive_title and the same conditional write the
title_supply_run job uses; a failed title is logged and the list is
frozen untitled, keeping its week label.
GET /api/v1/shopping/archive?by=week|month|year returns the Supply runs
up to the household's current week, newest first, grouped by the
Monday's ISO week, month or year, each post with its week label, title or
null, dinner, item and on-hand counts, dinner names and current flag. Any
other by is 400 invalid_request. Frozen lists stay readable through
GET /shopping/{weekStart} and reject item PATCHes with 409 archived.
Maestro task 7ac36526-cb51-4dc7-bbad-6ebbee481942.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The archive_title single-flight in the harness ended before a title job
stored its title, so a freeze in that gap ran a second model call and,
when it failed, froze the list untitled while the job's title was then
rejected. A title job now holds the service's titling lock shared from
reading the list to storing its title, and Freeze holds it exclusively
from reading the list to freezing it: a freeze waits for in-flight
titles to be stored or fail, and no title job calls the model while a
freeze runs. Adds controlled tests for both start orders, each paused
after archive_title returns and before its title is stored.
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.
Task
Maestro task
7ac36526-cb51-4dc7-bbad-6ebbee481942— "Week rollover freeze and archive API" (Feature 17, task 4 of 7).Done-condition
shopping.Freeze(weekStart)sets status archived andfrozen_atfor a list whose week has ended (idempotent); archived lists reject item PATCHes with 409.GET /api/v1/shopping/archive?by=week|month|yearreturns posts newest first grouped by the requested period (month/year headings in the household timezone), each post{weekStart, week label, title (or null), dinner count, item count, on-hand count, dinner names, current};GET /shopping/{weekStart}serves archived lists read-only. Tests cover freeze idempotency, 409 after freeze, grouping across a year boundary and the counts.Scope
The freeze service and the archive API. No UI.
Decisions the developer documented — please judge each
archive_titleskill, scope, payload source and conditional write thetitle_supply_runjob uses — no second mechanism. If titling fails, the freeze still goes ahead, the failure is logged, and the list keeps the week label for good. If atitle_supply_runjob is in flight,harness.Guardshares the in-flight run rather than making a second model call. Cost:Freezecan now make one model call bounded by the harness timeout.household.WeekLabelgives "Week 41 · Oct 5 – 11" (the mixed-case form the other APIs already return); mock 1m shows "WEEK 41 · OCT 5 – 11", which the developer expects the UI to produce with CSS capitals. Judge whether one label format is right.by=weekkeys ISO weeks ("2026-W53" with the week label as its heading), months "2026-10"/"October 2026", years "2026".needed=false.Other points for review
Freezeidempotency (frozen_atunchanged on a second freeze), explicit errors for a week not ended and a week with no list, the 409 on PATCH after freeze, and the read-only serve.byaccepts exactly week|month|year; anything else 400. The exact post field set.FreezeShoppingList, which only updates whilefrozen_at IS NULL, andListShoppingListsThrough) committed and not hand-edited; no migration.SetClockviaexport_test.go, followinginternal/gcal) — confirm it is test-only and does not change production behaviour.Evidence (orchestrator-run gate at
6da5643f49)gofmt -l .— empty;go vet ./...— cleango test -race -count=1 ./...— 27 packages ok, 0 failures (shopping 12.0s, server 187.4s)CGO_ENABLED=0 go build ./...— okmake sqlc-check SQLC=$(go env GOPATH)/bin/sqlc— no driftcd web && npm run typecheck && npm test && npm run build— PASS (no web change)Implementation commit:
6da5643f493a901c65d0c4544c20939a3f3d9f2cshopping.Freeze(weekStart) archives a week's Supply run once its week has ended in the household's time zone, setting status archived and frozen_at; it is idempotent, and a week not yet ended (ErrWeekNotEnded) or with no list (ErrListNotFound) is an explicit error. A list still untitled is titled first with archive_title and the same conditional write the title_supply_run job uses; a failed title is logged and the list is frozen untitled, keeping its week label. GET /api/v1/shopping/archive?by=week|month|year returns the Supply runs up to the household's current week, newest first, grouped by the Monday's ISO week, month or year, each post with its week label, title or null, dinner, item and on-hand counts, dinner names and current flag. Any other by is 400 invalid_request. Frozen lists stay readable through GET /shopping/{weekStart} and reject item PATCHes with 409 archived. Maestro task 7ac36526-cb51-4dc7-bbad-6ebbee481942. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.
Independent Codex gpt-6-sol review: APPROVED for this exact commit.