# CartWise Scrum Backlog

## Purpose

This document translates the product and implementation guidance from `AGENTS.md` into a Scrum-friendly backlog for CartWise v1. It organizes work into epics, user stories, and implementation tasks that align with the required phased delivery order.

This backlog is optimized for a solo founder + AI delivery model targeting a local MVP by **September 10, 2026**.

## Product Vision

CartWise is a mobile-first grocery companion web app that helps households remember what to buy, what they bought, what they paid, and what they may need again soon.

## Scrum Workflow

### Solo Cadence

- Planning cadence: weekly
- Delivery rhythm: one active story at a time
- Daily check-in: review top 3 priorities and blockers
- Backlog refinement: once per week
- Review/demo: every Friday
- Retrospective: every 2 weeks

### Working Model

- **Founder / Product Owner**: prioritizes scope, clarifies acceptance criteria, reviews AI output, and decides trade-offs
- **AI Development Partner**: scaffolds, implements, documents, and proposes follow-up tasks within approved scope
- **Primary QA**: founder validates behavior locally before marking stories done

### Board Columns

- Backlog
- Ready
- In Sprint
- In Progress
- Code Review
- Verify
- Done

### Estimation

- Use `XS`, `S`, `M`, and `L` sizing on stories
- Split any story larger than `L` before implementation
- Track tasks as checklist items and keep them aligned with the repository state

## Definitions

### Definition of Ready

A story is ready when:

- Business value is clear
- Acceptance criteria are written
- Dependencies are identified
- UI, domain, and data impact are understood
- Test approach is known
- The story is small enough to deliver in a sprint

### Definition of Done

A story is done when:

- Code is implemented
- Tests are added or updated
- Build passes
- Authorization and validation are enforced where applicable
- Logging is added where useful
- User-facing behavior is verified
- Work remains aligned with `AGENTS.md` architecture and scope

## Tracking Conventions

- Use `- [ ]` for not started tasks
- Use `- [x]` for completed tasks
- Update story acceptance criteria or add a short status note when a story is fully delivered
- Keep task state aligned with the actual repository state

## Release Strategy

- `Release: MVP` means required for the local launch target by **September 10, 2026**
- `Release: Post-MVP` means explicitly deferred until after MVP is stable
- `Release: Deferred` means do not implement unless scope changes
- `Priority: P0` means must ship for MVP
- `Priority: P1` means valuable next but can slip
- `Priority: P2` means later or exploratory
- `Effort: XS` under 2 hours, `S` about half a day, `M` about a day, `L` 2 or more days
- `Decision Gate` calls out places where the founder must make a product or architecture decision before implementation continues

## Epic Roadmap

### MVP Release
1. `CW-EPIC-01` Platform Foundation
2. `CW-EPIC-02` Household and Access Control
3. `CW-EPIC-03` Smart Shopping List
4. `CW-EPIC-05` Stores, Purchases, and Price Intelligence
5. `CW-EPIC-07` Running-Low Suggestions
6. Selected hardening work from `CW-EPIC-08`

### Post-MVP Release
1. `CW-EPIC-04` Product Catalog and Barcode Resolution
2. `CW-EPIC-06` Shopping Mode
3. Remaining polish work from `CW-EPIC-08`

---

## `CW-EPIC-01` Platform Foundation

**Goal:** establish the solution structure, authentication, SQLite wiring for MVP, and baseline engineering standards.

### `CW-STORY-01.1` Create solution skeleton
**Release:** MVP  
**Priority:** P0  
**Effort:** S
**User story:** As a developer, I want the CartWise solution scaffolded so the app follows a clean modular architecture.

**Acceptance criteria**
- `CartWise.sln` exists
- `src/` and `tests/` project layout matches the spec
- Project references follow approved dependency direction
- Nullable reference types are enabled

**Tasks**
- [x] Create `CartWise.sln`
- [x] Create `src/CartWise.Web`
- [x] Create `src/CartWise.Application`
- [x] Create `src/CartWise.Domain`
- [x] Create `src/CartWise.Infrastructure`
- [x] Create `tests/CartWise.Domain.Tests`
- [x] Create `tests/CartWise.Application.Tests`
- [x] Create `tests/CartWise.Web.Tests`
- [x] Add project references
- [x] Enable nullable reference types

**Status:** Done — solution builds and baseline template tests pass across all three test projects. `CartWise.Web` references `CartWise.Infrastructure` in addition to `CartWise.Application` for DI/startup registration (allowed per `AGENTS.md` §22 Phase 0).

### `CW-STORY-01.2` Configure application startup
**Release:** MVP  
**Priority:** P0  
**Effort:** M
**User story:** As a developer, I want a runnable MVC shell so the team can build on a working baseline.

**Acceptance criteria**
- ASP.NET Core MVC app starts locally
- Shared layout and static assets load correctly
- Environment-based configuration is wired
- SQLite connection is configurable for MVP
- PostgreSQL migration path remains possible later

**Tasks**
- [x] Configure MVC services and middleware
- [x] Add base layout and navigation shell
- [x] Organize `wwwroot` structure
- [x] Add configuration bindings
- [x] Wire SQLite connection string for MVP
- [x] Note PostgreSQL transition path in docs
- [x] Verify local app startup

**Status:** Done — app starts and serves the rebranded layout and static assets on a local port (verified via `dotnet run` + curl smoke check). `CartWiseDbContext` (empty for now — entities land in `CW-EPIC-02`) is wired to SQLite through `CartWise.Infrastructure.InfrastructureServiceCollectionExtensions.AddInfrastructure`, using `ConnectionStrings:DefaultConnection` in `appsettings.json`. PostgreSQL transition path documented in `docs/decision-log.md` under `DEC-005`.

### `CW-STORY-01.3` Add Identity authentication
**Release:** MVP  
**Priority:** P0  
**Effort:** M
**User story:** As a user, I want to sign in so my household data is protected.

**Acceptance criteria**
- Registration and sign-in are available
- Authenticated routes can be protected
- Identity persistence is configured

**Tasks**
- [x] Add ASP.NET Core Identity
- [x] Create `ApplicationUser`
- [x] Configure Identity persistence
- [x] Scaffold auth UI
- [x] Verify login and logout flow

**Status:** Done — `ApplicationUser : IdentityUser` (`DisplayName`, `CreatedUtc`) lives in `CartWise.Infrastructure.Identity`; `CartWiseDbContext` is now `IdentityDbContext<ApplicationUser>`. Identity is registered via `AddInfrastructure` (`AddIdentity` + `AddEntityFrameworkStores<CartWiseDbContext>`). Auth UI is a conventional MVC `AccountController` (`Register`/`Login`/`Logout`) with matching Razor views under `Views/Account`, anti-forgery on all POSTs, open-redirect guarded via `Url.IsLocalUrl`. `HomeController.Privacy` carries `[Authorize]` as the protected-route proof. Initial `InitialIdentity` EF Core migration created in `CartWise.Infrastructure/Data/Migrations`; the Development environment auto-applies pending migrations on startup (`Program.cs`) so local setup needs no manual `dotnet ef database update` step. Verified end-to-end via a live `dotnet run` + curl pass: anonymous access to a protected route redirects to `/Account/Login?ReturnUrl=...`, register auto-signs-in, logout clears the session, login re-authenticates, and a wrong password is rejected with a clear error.

### `CW-STORY-01.4` Establish engineering baseline
**Release:** MVP  
**Priority:** P1  
**Effort:** S
**User story:** As a team, we want build and test standards so changes remain stable over time.

**Acceptance criteria**
- Build can run consistently
- Basic test execution is available
- Logging defaults are configured

**Tasks**
- [x] Add analyzer and formatting settings
- [x] Configure structured logging baseline
- [x] Add smoke test coverage
- [x] Add CI-oriented build and test commands to docs

**Status:** Done — `Directory.Build.props` centralizes `Nullable`, `ImplicitUsings`, and .NET analyzers (`EnableNETAnalyzers`, `AnalysisLevel=latest-recommended`, `EnforceCodeStyleInBuild`) across all 7 projects; per-project `.csproj` files no longer duplicate those settings. Root `.editorconfig` added (generated via `dotnet new editorconfig`), with a scoped override so `CA1707` doesn't fight the xUnit `Method_Scenario_Expected` naming convention under `tests/`. Logging defaults tuned in `appsettings.json` (quiet `Microsoft.EntityFrameworkCore`/`Identity` categories) and `appsettings.Development.json` (`Microsoft.EntityFrameworkCore.Database.Command: Information` to see generated SQL locally). Smoke coverage added where there's real behavior to smoke — a `CartWiseWebApplicationFactory`-based integration suite (`tests/CartWise.Web.Tests/SmokeTests.cs`) boots the full app (DI, EF Core/SQLite, Identity, routing) against an isolated temp SQLite file and asserts: home page loads, an anonymous request to an `[Authorize]` route redirects to `/Account/Login`, and both auth pages load. `CartWise.Domain.Tests`/`CartWise.Application.Tests` still carry only the template placeholder test — there's no real domain/application logic yet to smoke-test; meaningful coverage there starts with `CW-STORY-02.1`. CI-oriented commands documented in `README.md` (`dotnet restore/build/test` against `CartWise.sln`; analyzers enforced at build time, not a separate lint step).

---

## `CW-EPIC-02` Household and Access Control

**Goal:** enable household creation, membership, and household-scoped authorization.

### `CW-STORY-02.1` Model households and membership
**Release:** MVP  
**Priority:** P0  
**Effort:** S
**User story:** As a developer, I want household entities persisted so household features have a secure foundation.

**Acceptance criteria**
- Household and membership entities exist
- Roles are defined
- EF configurations and migration are created

**Tasks**
- [x] Create `Household` entity
- [x] Create `HouseholdMember` entity
- [x] Add role enum
- [x] Add EF configurations
- [x] Create migration
- [x] Add domain tests

**Status:** Done — `Household` and `HouseholdMember` live in `CartWise.Domain.Entities`, `HouseholdRole` (`Owner`/`Member`) in `CartWise.Domain.Enums`. `Household` is the aggregate root: its constructor auto-creates an `Owner` membership for the creator, `AddMember` rejects duplicate users, and `HouseholdMember`'s constructor is `internal` so members can only be created through the aggregate — Domain stays free of any ASP.NET Core/Identity reference (`CreatedByUserId`/`UserId` are plain strings, not `ApplicationUser` navigations). `HouseholdConfiguration`/`HouseholdMemberConfiguration` in `CartWise.Infrastructure/Data/Configurations` establish the real FK to `AspNetUsers` (`Restrict` on `Household.CreatedByUserId` to protect household history from user-deletion cascades; `Cascade` on `HouseholdMember` rows), a composite PK on `(HouseholdId, UserId)`, and an index on `HouseholdMember.UserId` for "which households does this user belong to" lookups. `AddHousehold` migration created and verified applying cleanly against a scratch SQLite database (and implicitly on every Web smoke test run, since those auto-migrate on boot). 11 domain tests added covering the invariants above.

### `CW-STORY-02.2` Implement household service
**Release:** MVP  
**Priority:** P0  
**Effort:** M
**User story:** As a developer, I want household operations centralized so controllers stay thin and rules stay testable.

**Acceptance criteria**
- Household creation is supported
- Current household can be resolved
- Membership checks are reusable

**Tasks**
- [x] Create `IHouseholdService`
- [x] Implement `HouseholdService`
- [x] Add create household use case
- [x] Add get current household use case
- [x] Add membership verification logic
- [x] Add application tests

**Status:** Done — `IHouseholdService`/`HouseholdService` live in `CartWise.Application.{Interfaces,Services}`, exposing `CreateHouseholdAsync` (returns `Result<Household>`), `GetCurrentHouseholdAsync`, and `IsMemberAsync` (the reusable membership check `CW-STORY-02.4` will build authorization on top of). One household per user is enforced at this layer — see `DEC-009`. Data access goes through a new `IApplicationDbContext` abstraction (`CartWise.Application.Interfaces`, exposing just the `DbSet<Household>`/`DbSet<HouseholdMember>` needed) rather than a generic repository, since AGENTS.md explicitly rules out "generic repository abstractions added only for pattern compliance" — `CartWiseDbContext` implements it directly and is registered against the interface in `AddInfrastructure`. Time comes from the built-in `TimeProvider` (registered as `TimeProvider.System`) rather than a bespoke clock abstraction, keeping `CreateHouseholdAsync` deterministically testable without extra packages. `AddApplication()` (new `CartWise.Application` DI extension) is called from `Program.cs` alongside `AddInfrastructure`. 6 application tests added using EF Core's InMemory provider against the real `CartWiseDbContext` (via a new `CartWise.Infrastructure` reference in `CartWise.Application.Tests`, scoped to tests only — production `CartWise.Application` still has zero reference to `CartWise.Infrastructure`).

### `CW-STORY-02.3` Build household UI
**Release:** MVP  
**Priority:** P0  
**Effort:** S
**User story:** As a user, I want to create and view my household so I can begin using CartWise.

**Acceptance criteria**
- Household create page is available
- Household overview page is available
- Validation errors are shown clearly

**Tasks**
- [x] Create `HouseholdController`
- [x] Create household view models
- [x] Build `Views/Household/Create.cshtml`
- [x] Build `Views/Household/Index.cshtml`
- [x] Add validation messaging
- [x] Add redirect flow after creation

**Status:** Done — `HouseholdController` (`[Authorize]`, attribute-routed to `/household` and `/household/create` per AGENTS.md §11) with `HouseholdIndexViewModel`/`HouseholdMemberViewModel`/`CreateHouseholdViewModel` in `CartWise.Web.ViewModels.Household`. `Index` resolves member display names via `UserManager<ApplicationUser>` in the controller (not the Application layer, to keep `CartWise.Application` free of Identity references) and redirects to `Create` when the signed-in user has no household yet; `Create` redirects back to `Index` if a household already exists (enforcing `DEC-009`'s one-per-user rule at the UI layer too, on top of the service-layer guard). Added a "Household" nav link, visible only when authenticated. Only Create + Index were built — member invite/remove (`/household/members/*`) are explicitly out of scope per AGENTS.md, which marks invite `[future/simple v1 if desired]`, and `HouseholdService` has no add/remove-member use case yet to back them. Verified live end-to-end via `dotnet run` + curl: anonymous-to-registered-to-household-created-to-overview-shown flow, member display name and `Owner` role render correctly, revisiting `/household/create` after a household exists redirects to `/household`, and an empty-name submission redisplays the form with visible validation errors (200, not a redirect).

### `CW-STORY-02.4` Enforce household authorization
**Release:** MVP  
**Priority:** P0  
**Effort:** S
**User story:** As a user, I want household data isolated so other users cannot access it.

**Acceptance criteria**
- Household membership is enforced on household-scoped resources
- Unauthorized requests are blocked
- Resource isolation is covered by tests

**Tasks**
- [x] Add household authorization policy or helper
- [x] Enforce membership checks in services
- [x] Add integration tests for access control
- [x] Add tests for unauthorized and cross-household access

**Status:** Done — added a reusable resource-based `"HouseholdMember"` authorization policy: `HouseholdMemberRequirement` + `HouseholdMemberAuthorizationHandler` (`CartWise.Web.Authorization`), backed by the `IHouseholdService.IsMemberAsync` check already built in `CW-STORY-02.2`, registered via `AddAuthorizationBuilder()` in `Program.cs`. Note on current scope: `HouseholdController`'s existing actions (`Index`/`Create`) never accept a client-supplied household id — they always resolve "my household" from the authenticated user's own membership — so there's no IDOR surface to protect on them today; the new policy exists so the first household-scoped resource that *does* take an id (starting with `CW-EPIC-03`'s shopping list) can apply `[Authorize(Policy = "HouseholdMember")]`/resource-based `AuthorizeAsync` immediately instead of hand-rolling the check. Verified with: a unit-style test exercising the policy through the real DI container (`HouseholdAuthorizationTests` — two real users/households, confirms the policy succeeds for the owning member and fails for a user from a different household), and Web integration tests (`HouseholdAccessControlTests`) proving an anonymous request to `/household` redirects to login, and that two independently-registered users each only ever see their own household's name on `/household` — never each other's.

---

## `CW-EPIC-03` Smart Shopping List

**Goal:** allow household members to maintain a shared grocery list, including unresolved free-text items.

### `CW-STORY-03.1` Model shopping list and grocery concepts
**Release:** MVP  
**Priority:** P0  
**Effort:** M
**User story:** As a developer, I want shopping list entities defined so shared list workflows can be persisted.

**Acceptance criteria**
- Grocery concept, category, shopping list, and shopping list item entities exist
- Item statuses are defined
- Optimistic concurrency is supported for list items

**Tasks**
- [x] Create `GroceryConcept`
- [x] Create `ProductCategory`
- [x] Create `ShoppingList`
- [x] Create `ShoppingListItem`
- [x] Add list status enums
- [x] Add row version concurrency token
- [x] Add EF configurations
- [x] Create migration

**Status:** Done — `ProductCategory`, `GroceryConcept`, `ShoppingList` (aggregate root with an `Items` backing-field navigation, mirroring `Household`/`HouseholdMember`), and `ShoppingListItem` added in `CartWise.Domain.Entities`; `ShoppingListStatus` (`Active`/`Completed`/`Archived`) and `ShoppingListItemStatus` (`Needed`/`Purchased`/`Skipped`/`Unavailable`/`Substituted`) in `CartWise.Domain.Enums`. Concurrency: `ShoppingListItem.RowVersion` is a `Guid` configured with `.IsConcurrencyToken()` rather than `.IsRowVersion()` — SQLite has no native auto-generated rowversion column, and `IsConcurrencyToken()` works identically on SQLite and a future PostgreSQL move (EF includes the original value in the `WHERE` clause either way); regenerating it on mutation is deferred to `CW-STORY-03.4`, which is where mutation methods land. Scope note: unlike `Household.AddMember`, no `ShoppingList.AddItem(...)` aggregate method exists yet — `ShoppingListItem`'s constructor is public for now since 03.1 is pure modeling and `CW-STORY-03.3` owns the add-item use case; that story should consider tightening `ShoppingListItem`'s constructor to `internal` behind an aggregate method, matching the `Household` pattern. `ShoppingListItem.PreferredProductId` from the AGENTS.md §5.4 spec was intentionally omitted — `Product` doesn't exist until the post-MVP `CW-EPIC-04`, and MVP explicitly doesn't require product resolution; it will be added via a future migration once `Product` exists. `AddShoppingList` migration created and verified applying cleanly against a scratch SQLite database.

### `CW-STORY-03.2` Implement active shopping list service
**Release:** MVP  
**Priority:** P0  
**Effort:** S
**User story:** As a user, I want my household to have an active grocery list so shared planning is easy.

**Acceptance criteria**
- Active list can be retrieved
- Default active list can be created
- Household rule for default active list is enforced

**Tasks**
- [x] Create `IShoppingListService`
- [x] Implement get active list
- [x] Implement create list
- [x] Enforce one default active list rule
- [x] Add application tests

**Status:** Done — `IShoppingListService`/`ShoppingListService` in `CartWise.Application.{Interfaces,Services}`, following the exact pattern established by `HouseholdService` in `CW-STORY-02.2`: `GetActiveListAsync` (read, includes `Items`) and `CreateListAsync` (returns `Result<ShoppingList>`, rejects a second active list per the AGENTS.md §5.4 v1 rule with a clear error message). `IApplicationDbContext` extended with `ShoppingLists`/`ShoppingListItems` DbSets. Registered in `AddApplication()`. 6 application tests added (EF InMemory, same harness as `HouseholdServiceTests`), including a cross-household isolation check mirroring the pattern from `CW-STORY-02.4`. Also fixed a `CA1861` warning surfaced by the `AddShoppingList` migration's composite-column indexes by scoping a suppression to `Data/Migrations/**` in `.editorconfig` — migrations are generated code and shouldn't be hand-edited to satisfy analyzers.

### `CW-STORY-03.3` Add grocery items quickly
**Release:** MVP  
**Priority:** P0  
**Effort:** S
**User story:** As a household member, I want to add free-text grocery items quickly so I do not lose shopping intent.

**Acceptance criteria**
- Free-text item entry is supported
- Quantity and unit are optional
- Item appears on the active list
- Product resolution is not required to add an item

**Tasks**
- [x] Implement add item use case
- [x] Support `DisplayName`, quantity, and unit
- [x] Add validation rules
- [x] Add item partial view
- [x] Add controller POST action
- [x] Add tests

**Status:** Done — `ShoppingList.AddItem(...)` aggregate method added (mirrors `Household.AddMember`; `ShoppingListItem`'s constructor is now `internal`), computing `SortOrder` and rejecting adds to a non-active list. `IShoppingListService.AddItemAsync` added. `ShoppingListController` (`GET /list`, `POST /list/items`) auto-provisions a household's first list (named "Groceries") on first visit — no separate "create list" UI step, matching AGENTS.md's "one active list is enough for MVP" and the First End-to-End Slice in `CLAUDE.md`. `Views/ShoppingList/Index.cshtml` + `_ListItem.cshtml` built (functional, Bootstrap-styled; the dedicated mobile-first/touch-friendly pass is `CW-STORY-03.5`). Scope note carried from `CW-STORY-03.1`'s status resolved: the constructor tightening happened here as anticipated.

Two real bugs found and fixed via testing, not just the earlier apostrophe-encoding test bug:
1. **EF Core state-tracking bug**: adding a new `ShoppingListItem` purely by mutating the already-tracked parent `ShoppingList`'s backing-field collection (`list.AddItem(...)`) left the new item's state ambiguous to EF Core's change tracker — since `ShoppingListItemId` is a client-generated `Guid` (not store-generated), EF couldn't reliably infer `Added` vs `Modified`, and attempted an `UPDATE` on a row that didn't exist yet, throwing `DbUpdateConcurrencyException`. Fixed by explicitly calling `_db.ShoppingListItems.Add(item)` in `ShoppingListService.AddItemAsync` after `list.AddItem(...)`.
2. **Model-binding prefix mismatch**: `Views/ShoppingList/Index.cshtml`'s tag helpers generate field names like `NewItem.DisplayName` (based on `ShoppingListViewModel.NewItem`'s property path), but `ShoppingListController.AddItem` bound a bare, unprefixed `AddShoppingListItemViewModel model` parameter — so nothing bound, `DisplayName` silently came through empty, and (surprisingly) no validation error surfaced either. Fixed with `[Bind(Prefix = "NewItem")]` on the action parameter.

Both were caught by genuine integration tests (`ShoppingListAccessControlTests`, real HTTP + real SQLite) written for this story, not by the weaker EF-InMemory application tests alone — worth remembering for future controller/view-model pairs that use a nested form-section pattern like this one. Extracted `WebTestHelpers` (register/create-household/extract-antiforgery-token) out of `HouseholdAccessControlTests` into a shared file so `ShoppingListAccessControlTests` didn't duplicate it. Verified live end-to-end via `dotnet run` + curl, matching `CLAUDE.md`'s First End-to-End Slice exactly: register → create household → `/list` auto-creates "Groceries" → add "Milk" (2 gal) → reload shows it with `Needed` status; empty-name submission correctly redisplays the form with a visible validation error (200, not a redirect).

### `CW-STORY-03.4` Update shopping list items
**Release:** MVP  
**Priority:** P0  
**Effort:** M
**User story:** As a household member, I want to toggle, skip, and delete items so the list stays accurate.

**Acceptance criteria**
- Items can be marked purchased
- Items can be skipped and unskipped
- Items can be deleted
- Concurrency conflicts are handled safely

**Tasks**
- [x] Implement toggle purchased
- [x] Implement skip and unskip
- [x] Implement delete item
- [x] Handle concurrency exceptions
- [x] Add progressive enhancement with jQuery
- [x] Preserve server-post fallback
- [x] Add tests

**Status:** Done — `ShoppingListItem.TogglePurchased`/`ToggleSkipped` domain methods added (each regenerates `RowVersion`); `POST /list/items/{id}/toggle` and `/skip` are true toggles (one route each, per AGENTS.md §11 — no separate "unpurchase"/"unskip" routes), `/delete` hard-deletes (no "Deleted" status exists, and list items aren't history — unlike `PriceObservation`, nothing here needs to stay append-only). `IShoppingListService.TogglePurchasedAsync`/`ToggleSkippedAsync`/`DeleteItemAsync` all take `householdId` and resolve the item via a household-scoped join query (`FindItemForHouseholdAsync`) rather than trusting the route-supplied item id alone — an item belonging to a different household is indistinguishable from "not found," so no existence is leaked. On reflection this made the `"HouseholdMember"` authorization policy from `CW-STORY-02.4` unnecessary here: query-scoping is atomic (one query both finds the item and enforces ownership) and matches the pattern already used everywhere else in the codebase, so it wasn't invoked for this story — it remains available and tested for a future scenario that needs to authorize before it can even construct the right query.

**Concurrency handling**: the client-rendered `rowVersion` hidden field round-trips through the form; the service sets `_db.Entry(item).Property(i => i.RowVersion).OriginalValue = expectedRowVersion` before mutating, so EF Core's generated `UPDATE`/`DELETE` includes the *client's* last-seen value in its `WHERE` clause (the standard EF Core disconnected-entity concurrency pattern) — a stale submission throws `DbUpdateConcurrencyException`, caught and surfaced as `Result.Failure(...)`, never a 500.

**Progressive enhancement**: `wwwroot/js/pages/shopping-list.js` intercepts the toggle/skip/delete forms' submit via jQuery, POSTs via `$.post` (which sets `X-Requested-With: XMLHttpRequest` automatically), and the controller detects that header to return either the updated `_ListItem` partial (toggle/skip) or `204`/`409` (delete) instead of a redirect — `Views/ShoppingList/_ListItem.cshtml`'s three per-item forms work unmodified via plain server POST if JS is unavailable, satisfying "standard form posts must work where practical."

Two more real bugs found by tests (bringing the running total for `CW-EPIC-03` to four — see `CW-STORY-03.3`'s notes for the first two): both were test-assertion bugs on my part, not product bugs — `Assert.DoesNotContain("Purchased", html)` false-failed because the *toggle button's label* is literally "Purchased" on an unpurchased item (`isPurchased ? "Undo" : "Purchased"`), unrelated to the actual status badge. Fixed by asserting against the specific `badge bg-secondary">{status}</span>` markup instead of a bare substring — worth remembering for `CW-STORY-03.5` and beyond: any status-driven UI where the action-button label overlaps with a possible status name needs a precise assertion, not `Contains`/`DoesNotContain` on the raw status word. Verified live end-to-end via `dotnet run` + curl: toggle Needed→Purchased shown in the status badge with the correct `rowVersion`, and an AJAX request (`X-Requested-With: XMLHttpRequest`) with a deliberately stale `rowVersion` correctly returns `409`.

### `CW-STORY-03.5` Deliver a mobile-first list UI
**Release:** MVP  
**Priority:** P0  
**Effort:** S
**User story:** As a shopper, I want a touch-friendly grocery list so it works well on my phone.

**Acceptance criteria**
- List page is readable on mobile
- Quick add is prominent
- Tap targets are appropriate for shopping use

**Tasks**
- [x] Build `Views/ShoppingList/Index.cshtml`
- [x] Build `Views/ShoppingList/_ListItem.cshtml`
- [x] Add quick-add UI
- [x] Add touch-friendly styles
- [x] Add `wwwroot/js/pages/shopping-list.js`

**Status:** Done, with a verification caveat — the Index/_ListItem views, quick-add UI, and `shopping-list.js` already existed functionally from `CW-STORY-03.3`/`03.4`; this story was the dedicated mobile-first/touch-friendly polish pass. Changes: (1) fixed `site.css`'s inverted base font size (was 14px on mobile, 16px on desktop — backwards, and inputs under 16px trigger iOS Safari's zoom-on-focus) to a flat 16px; (2) added `.shopping-list-quick-add`/`.shopping-list-item`/`.shopping-list-item-actions` CSS establishing ~44px-minimum touch targets on every button (removed `btn-sm`) and a 3rem-tall, 1.1rem-font quick-add input/button pair; (3) `NewItem.DisplayName` is now `col-12` at every breakpoint (not just mobile) so it's unambiguously the prominent primary action, with Quantity/Unit/Add sharing a secondary row below; (4) added `inputmode="decimal"` to the Quantity field so mobile browsers show a numeric keypad instead of a full keyboard; (5) narrow viewports (`max-width: 575.98px`) stack each list item's name above its actions instead of cramming them into one row.

**Verification caveat:** no browser-automation tool (Playwright/`chromium-cli`) is available in this environment, so this could not be visually screenshotted at a mobile viewport as the production-readiness guidance calls for. What *was* verified: `dotnet run` + curl confirms the rendered HTML carries the new classes/attributes exactly as written, the CSS file is served and contains the expected rules, and the full test suite (67 tests, unaffected by these purely-visual changes) still passes. The founder should do a quick real-device or DevTools-mobile-emulation pass before treating this as fully done — the CSS follows standard, well-established sizing conventions (WCAG 2.5.5's ~44px target, 16px inputs) but hasn't been eyeballed.

---

## `CW-EPIC-04` Product Catalog and Barcode Resolution

**Goal:** support product identity, household product preferences, and local-first barcode resolution with provider fallback.

### `CW-STORY-04.1` Model product catalog entities
**Release:** Post-MVP  
**Priority:** P1  
**Effort:** M  
**Decision Gate:** Confirm whether a product catalog is still needed before barcode work begins.
**User story:** As a developer, I want product and identifier entities modeled so products can be resolved independently of UPC.

**Acceptance criteria**
- Brand, product, identifier, and household preference entities exist
- Identifier uniqueness rules are defined
- EF configurations and migration are created

**Tasks**
- [x] Create `Brand`
- [x] Create `Product`
- [x] Create `ProductIdentifier`
- [x] Create `HouseholdProductPreference`
- [x] Add source type enums
- [x] Add indexes and normalized fields
- [x] Add EF configurations
- [x] Create migration

**Status:** Done — per `DEC-010`, this epic is being built now ahead of MVP-tagged `CW-EPIC-05`/`07` at explicit founder direction (strict numeric epic order), overriding this story's own "confirm whether a product catalog is still needed" decision gate implicitly via that same direction. `Product` is the aggregate root for `ProductIdentifier` (`AddIdentifier`, mirroring `Household.AddMember`/`ShoppingList.AddItem`), rejecting a duplicate `(IdentifierType, NormalizedValue)` pair before it would ever hit the DB's unique index — same defense-in-depth pattern as `HouseholdService`'s one-active-list check. `ProductIdentifier.Normalize` is `public static` since `CW-STORY-04.3`'s barcode lookup will need the identical normalization before it ever constructs an identifier. `HouseholdProductPreference` enforces the `HouseholdId`+`GroceryConceptId` uniqueness from AGENTS.md §5.3 as a real unique index. `AddProductCatalog` migration verified applying cleanly.

### `CW-STORY-04.2` Search and view products
**Release:** Post-MVP  
**Priority:** P1  
**Effort:** M
**User story:** As a user, I want to search and view products so I can understand and choose preferred items.

**Acceptance criteria**
- Local product search works
- Product details page displays key product information
- Household price insights can be shown later on the details page

**Tasks**
- [x] Create `IProductService`
- [x] Implement local catalog search
- [x] Create `ProductController`
- [x] Build `Views/Product/Search.cshtml`
- [x] Build `Views/Product/Details.cshtml`
- [x] Add product view models
- [x] Add tests

**Status:** Done — `IProductService`/`ProductService` return `ProductSummaryDto`/`ProductDetailsDto` (new `CartWise.Application.DTOs`) rather than raw `Product` entities, since search/details need `BrandName`/`ConceptName` resolved via joins that don't belong on the Domain entity itself. Search is a simple `NormalizedName.Contains(...)` match, capped at 25 results. `ProductController` routes (`/products/search`, `/products/{id:guid}`) match AGENTS.md §11 exactly; `Details` 404s for an unknown id. Price/household-insight fields from AGENTS.md §12's `ProductDetailsViewModel` were intentionally omitted — the acceptance criteria itself says "can be shown *later*," and `CW-EPIC-05` (price intelligence) doesn't exist yet; the details page has a placeholder note instead of empty stub fields. 5 application tests + 3 Web tests added.

### `CW-STORY-04.3` Implement barcode lookup flow
**Release:** Deferred  
**Priority:** P2  
**Effort:** M  
**Decision Gate:** Barcode scanning is out of MVP scope.
**User story:** As a shopper, I want a barcode lookup flow so known products can be resolved quickly.

**Acceptance criteria**
- Barcode input is normalized
- Local identifier lookup runs first
- Provider fallback runs when local lookup misses
- Accepted results persist locally

**Tasks**
- [x] Define `IProductDataProvider`
- [x] Implement barcode normalization
- [x] Implement local-first lookup flow
- [x] Persist imported products and identifiers
- [x] Add unit and application tests

**Status:** Done — `IProductDataProvider` (`CartWise.Application.Interfaces`) matches AGENTS.md §9 exactly; `ProductLookupResult`/`ProductSearchResult` DTOs added. `IProductService.ResolveBarcodeAsync` implements the local-first flow: normalize (`ProductIdentifier.Normalize`, already built in `CW-STORY-04.1`) → local `ProductIdentifiers` lookup → iterate injected `IEnumerable<IProductDataProvider>` (empty until `CW-STORY-04.4` registers a real one — the flow is fully testable now via a `FakeProductDataProvider` test double) → persist `Product`+`ProductIdentifier` (tagged `Gtin` since the scanned format isn't always known) and a `Brand` if the provider returned one not already in the catalog → return the same `ProductDetailsDto` shape `CW-STORY-04.2` already established. 5 new application tests (local hit skips the provider entirely, provider fallback persists correctly with the right `SourceType`/`SourceExternalId`, not-found-anywhere fails cleanly, missing-barcode input fails cleanly).

### `CW-STORY-04.4` Integrate Open Food Facts
**Release:** Deferred  
**Priority:** P2  
**Effort:** M  
**Decision Gate:** External providers are out of MVP scope.
**User story:** As a developer, I want a provider fallback for unknown products so barcode resolution remains useful.

**Acceptance criteria**
- Open Food Facts provider can query by barcode
- Provider models remain isolated to infrastructure
- Provider errors are logged safely

**Tasks**
- [x] Implement `OpenFoodFactsProductDataProvider`
- [x] Map provider responses to internal DTOs
- [x] Add configuration and secrets support
- [x] Add error handling and logging
- [x] Add tests with mocked provider behavior

**Status:** Done — `OpenFoodFactsProductDataProvider` (`CartWise.Infrastructure.Integrations.OpenFoodFacts`) is registered as a typed `HttpClient` (`AddHttpClient<IProductDataProvider, OpenFoodFactsProductDataProvider>`), so `CW-STORY-04.3`'s `ProductService.ResolveBarcodeAsync` now actually has a provider in its `IEnumerable<IProductDataProvider>` instead of an empty list. Response models (`OpenFoodFactsResponse`/`OpenFoodFactsProduct`) are `internal` to Infrastructure — never referenced outside it, satisfying "never bind Razor pages directly to provider-specific response models." Base URL is configurable via `OpenFoodFacts:BaseUrl` in `appsettings.json` (no API key needed for OFF's public lookup endpoint, but the config-driven shape is ready for a future key-requiring provider like USDA). HTTP/JSON failures are caught narrowly (`HttpRequestException`, `JsonException`, `TaskCanceledException`) and logged via a source-generated `[LoggerMessage]` delegate rather than the ad-hoc `ILogger.LogWarning(...)` extension — a small perf-conscious pattern worth reusing in `CW-STORY-08.3`. A lightweight quantity parser extracts `SizeValue`/`SizeUnit` from OFF's free-text `quantity` field (e.g. `"16 oz"`), returning `null`/`null` rather than guessing when it doesn't parse. No dedicated Infrastructure test project exists (AGENTS.md's structure only lists Domain/Application/Web test projects), so — consistent with how `HouseholdServiceTests`/`ShoppingListServiceTests` already exercise real Infrastructure code — the 4 new tests live in `CartWise.Application.Tests`, using a hand-rolled fake `HttpMessageHandler` (found/not-found/HTTP-failure/malformed-JSON) rather than pulling in a mocking library.

### `CW-STORY-04.5` Build scan page
**Release:** Deferred  
**Priority:** P2  
**Effort:** M  
**Decision Gate:** Camera scan flow is out of MVP scope.
**User story:** As a shopper, I want a camera-based scan page so I can identify products with my phone.

**Acceptance criteria**
- Scan page requests camera permission
- Barcode result can resolve product details
- User can add scanned product to the list or record a price

**Tasks**
- [x] Create `ScanController`
- [x] Build `Views/Scan/Index.cshtml`
- [x] Add camera permission flow
- [x] Add barcode JS integration
- [x] Add add-to-list and record-price actions

**Status:** Done, with one deliberate partial: "record-price" isn't wired up because `CW-EPIC-05` (Purchases/Price Intelligence) hasn't been built yet — a direct consequence of `DEC-010`'s strict-numeric-order choice reaching `04.5` before `05` exists. "Add to list" is fully wired instead, which satisfies the acceptance criterion's "add ... to the list **or** record a purchase/price." Will revisit once `CW-EPIC-05`'s `PriceController` exists.

Also added `GET /products/barcode/{barcode}` and `GET /api/products/barcode/{barcode}` to `ProductController` (per AGENTS.md §11 — these were specified but not yet built in `CW-STORY-04.2`/`04.3`, since neither of those stories' task lists mentioned a controller route; the scan page is what actually needed them). The JSON route is the "actual client-side need" AGENTS.md §11 requires before introducing an `/api` route — the scan page's JS calls it via `fetch`/`$.getJSON` to resolve a detected barcode without a full page reload.

No barcode-scanning JS library is vendored — this project has no bundler/npm step (`wwwroot` is plain static files, matching the existing jQuery/Bootstrap vendoring pattern), and pulling in a third-party scanning library's browser build wasn't practical to do well without one. Instead, `wwwroot/js/pages/scan.js` uses the native browser `BarcodeDetector` API (Chrome/Edge/Android WebView) with graceful degradation: browsers lacking it get a clear "not supported" message and a manual barcode-entry text field instead, which also doubles as the non-JS fallback path per AGENTS.md §14.

Verified live end-to-end with a **real** barcode against the **real** Open Food Facts API (not mocked): `GET /api/products/barcode/3017620422003` (Nutella) → provider fallback fires → product persists locally with the correct name/brand/identifier → `/products/{id}` renders it → "Add to my list" persists it onto the household's shopping list. Also confirmed a genuinely-unknown barcode correctly 404s (HTML route) / returns `{"found":false}` (JSON route) after the provider fallback misses too.

---

## `CW-EPIC-05` Stores, Purchases, and Price Intelligence

**Goal:** record where purchases happened and build a trustworthy append-only price history.

### `CW-STORY-05.1` Model retailers and store locations
**Release:** MVP  
**Priority:** P1  
**Effort:** S  
**Decision Gate:** Decide whether store tracking in MVP is minimal or omitted from first purchase flow.
**User story:** As a developer, I want retailer and store entities so purchases can be tied to real locations.

**Acceptance criteria**
- Retailer and store entities exist
- Location fields support practical v1 store data
- EF configurations and migration are created

**Tasks**
- [x] Create `Retailer`
- [x] Create `StoreLocation`
- [x] Add EF configurations
- [x] Create migration
- [x] Add tests for basic persistence rules

**Status:** Done — per `DEC-011`, store tracking is optional everywhere it appears (`Purchase.StoreLocationId`/`PriceObservation.StoreLocationId` are nullable, as AGENTS.md §5.6 already specified for the latter). `Retailer`/`StoreLocation` are plain independent entities (no aggregate relationship needed — a store isn't composed/protected by its retailer the way `ShoppingListItem` is by `ShoppingList`). `AddRetailersAndStores` migration verified applying cleanly. 9 domain tests.

### `CW-STORY-05.2` Model purchases and purchase items
**Release:** MVP  
**Priority:** P0  
**Effort:** M
**User story:** As a developer, I want purchase history modeled so shopping outcomes can be stored.

**Acceptance criteria**
- Purchase and purchase item entities exist
- Purchase source types are defined
- EF configurations and migration are created

**Tasks**
- [ ] Create `Purchase`
- [ ] Create `PurchaseItem`
- [ ] Add source type enum
- [ ] Add EF configurations
- [ ] Create migration
- [ ] Add tests

### `CW-STORY-05.3` Model append-only price observations
**Release:** MVP  
**Priority:** P0  
**Effort:** S
**User story:** As a developer, I want append-only price observations so historical price facts are preserved.

**Acceptance criteria**
- Price observation entity exists
- Useful indexes are defined for history queries
- Older observations are not overwritten by newer ones

**Tasks**
- [ ] Create `PriceObservation`
- [ ] Add indexes for product, household, and store history
- [ ] Add EF configuration
- [ ] Create migration
- [ ] Add tests for append-only behavior

### `CW-STORY-05.4` Record household purchases
**Release:** MVP  
**Priority:** P0  
**Effort:** M  
**Decision Gate:** Decide whether MVP purchase entry must link directly to shopping list items or can begin as a manual flow.
**User story:** As a shopper, I want to record purchases so CartWise can remember what I bought and what I paid.

**Acceptance criteria**
- Purchase flow can start and complete
- Purchase items can be added
- Shopping list items can be linked when relevant

**Tasks**
- [ ] Create `IPurchaseService`
- [ ] Implement start purchase flow
- [ ] Implement add purchase item flow
- [ ] Implement complete purchase flow
- [ ] Link purchase items to shopping list items where applicable
- [ ] Add tests

### `CW-STORY-05.5` Derive price intelligence
**Release:** MVP  
**Priority:** P0  
**Effort:** M
**User story:** As a user, I want price history and price insights so I can make better grocery decisions.

**Acceptance criteria**
- Purchase items can create price observations
- Latest, average, median, lowest, and unit price calculations are supported when data allows
- Freshness and source can be displayed

**Tasks**
- [ ] Create `IPriceService`
- [ ] Derive observations from purchase items
- [ ] Implement latest price calculation
- [ ] Implement average and median calculations
- [ ] Implement lowest recent price calculation
- [ ] Implement unit price calculation
- [ ] Add tests for price statistics

### `CW-STORY-05.6` Build price views
**Release:** MVP  
**Priority:** P0  
**Effort:** S
**User story:** As a user, I want a price history screen so I can review what my household typically pays.

**Acceptance criteria**
- Prices index page exists
- Product-specific price history page exists
- Product details can show historical price insight

**Tasks**
- [ ] Create `PriceController`
- [ ] Build `Views/Price/Index.cshtml`
- [ ] Build `Views/Price/Product.cshtml`
- [ ] Add price history to product details view
- [ ] Add filtering and sorting UI
- [ ] Add freshness and source display

---

## `CW-EPIC-06` Shopping Mode

**Goal:** provide a simple, touch-friendly in-store workflow for marking progress and recording purchases.

### `CW-STORY-06.1` Implement shopping mode service
**Release:** Post-MVP  
**Priority:** P1  
**Effort:** S  
**Decision Gate:** Decide whether a dedicated shopping mode is needed beyond the list page.
**User story:** As a developer, I want shopping-mode orchestration so in-store behavior is consistent and testable.

**Acceptance criteria**
- Shopping mode view can be prepared from list data
- Items can be grouped for easier shopping
- Remaining and purchased counts are available

**Tasks**
- [ ] Create `IShoppingModeService`
- [ ] Implement shopping mode preparation
- [ ] Group by category-based sections
- [ ] Calculate remaining and purchased counts
- [ ] Add tests

### `CW-STORY-06.2` Build shopping mode UI
**Release:** Post-MVP  
**Priority:** P1  
**Effort:** M
**User story:** As a shopper, I want a low-noise mobile shopping view so I can use CartWise during a real trip.

**Acceptance criteria**
- Shopping mode works well on a phone
- Groups and counts are clear
- Key actions are easy to reach

**Tasks**
- [ ] Create `ShoppingController`
- [ ] Build `Views/Shopping/Start.cshtml`
- [ ] Build `Views/Shopping/_ShoppingItem.cshtml`
- [ ] Add touch-friendly controls
- [ ] Add `wwwroot/js/pages/shopping-mode.js`

### `CW-STORY-06.3` Support in-store item actions
**Release:** Post-MVP  
**Priority:** P1  
**Effort:** M
**User story:** As a shopper, I want to mark items purchased, skipped, unavailable, or substituted so the trip stays accurate.

**Acceptance criteria**
- Item transitions are supported
- Anti-forgery is enforced on state changes
- Actions update the shopping workflow correctly

**Tasks**
- [ ] Add purchase action
- [ ] Add skip action
- [ ] Add unavailable action
- [ ] Add substitute action
- [ ] Add anti-forgery for AJAX and form posts
- [ ] Add tests

### `CW-STORY-06.4` Record prices while shopping
**Release:** Post-MVP  
**Priority:** P1  
**Effort:** S
**User story:** As a shopper, I want to optionally enter prices in shopping mode so price history stays current.

**Acceptance criteria**
- Optional price entry is available during purchase actions
- Money input is validated
- Captured values feed purchase and price history logic

**Tasks**
- [ ] Add optional per-item price input
- [ ] Validate price and quantity inputs
- [ ] Persist captured values through purchase service
- [ ] Add tests

---

## `CW-EPIC-07` Running-Low Suggestions

**Goal:** use deterministic purchase history to suggest what a household may need soon.

### `CW-STORY-07.1` Build replenishment engine
**Release:** MVP  
**Priority:** P0  
**Effort:** M
**User story:** As a developer, I want deterministic running-low logic so suggestions are explainable and testable.

**Acceptance criteria**
- Last N purchase dates can be evaluated
- Median interval is used when sufficient history exists
- Minimum-history and tolerance rules are enforced

**Tasks**
- [ ] Create `IReplenishmentService`
- [ ] Gather recent purchase dates
- [ ] Calculate purchase intervals
- [ ] Implement median interval logic
- [ ] Add minimum-history thresholds
- [ ] Add tolerance window logic
- [ ] Return score and reason data
- [ ] Add tests

### `CW-STORY-07.2` Show suggestions on home and list views
**Release:** MVP  
**Priority:** P0  
**Effort:** S  
**Decision Gate:** Confirm the minimum launch dashboard: active list count, recent prices, and running-low suggestions.
**User story:** As a user, I want running-low suggestions surfaced in context so I can act on them quickly.

**Acceptance criteria**
- Home page can display suggestions
- Shopping list page can display suggestions
- Suggestions include reason or confidence context

**Tasks**
- [ ] Extend `HomeViewModel`
- [ ] Add suggestions to home controller flow
- [ ] Add running-low partial views
- [ ] Render suggestions on list page
- [ ] Add tests

### `CW-STORY-07.3` Add suggestion-to-list workflow
**Release:** MVP  
**Priority:** P1  
**Effort:** S
**User story:** As a user, I want to add a running-low suggestion to my list so I can convert insight into action quickly.

**Acceptance criteria**
- Suggestion can be added to the active list
- Existing list workflows are reused
- Duplicate accidental adds are reasonably controlled

**Tasks**
- [ ] Add controller action to add suggestion
- [ ] Reuse shopping list add-item workflow
- [ ] Add duplicate-prevention rules if needed
- [ ] Add tests

---

## `CW-EPIC-08` UX Polish, Security, and Release Readiness

**Goal:** harden the v1 experience for real-world use and release confidence.

### `CW-STORY-08.1` Improve mobile usability and accessibility
**Release:** MVP  
**Priority:** P1  
**Effort:** S
**User story:** As a user, I want the app to be easy to use on my phone and accessible across core workflows.

**Acceptance criteria**
- Key pages are responsive
- Labels, focus states, and contrast meet baseline usability needs
- Error handling is understandable

**Tasks**
- [ ] Review responsive layouts
- [ ] Improve form labels and focus states
- [ ] Improve contrast and tap sizes
- [ ] Validate keyboard navigation
- [ ] Validate error summaries

### `CW-STORY-08.2` Harden security coverage
**Release:** MVP  
**Priority:** P0  
**Effort:** S
**User story:** As a team, we want security-sensitive paths reviewed so household data remains protected.

**Acceptance criteria**
- Anti-forgery coverage is verified
- Authorization coverage is verified
- Secrets handling is consistent with app rules

**Tasks**
- [ ] Review anti-forgery coverage
- [ ] Review authorization enforcement
- [ ] Review validation across state-changing flows
- [ ] Review secrets and configuration handling
- [ ] Add integration tests for protected routes

### `CW-STORY-08.3` Add diagnostics and logging
**Release:** MVP  
**Priority:** P1  
**Effort:** S
**User story:** As a team, we want actionable diagnostics so failures can be understood and fixed quickly.

**Acceptance criteria**
- External provider failures are logged
- Barcode misses can be diagnosed
- Authorization failures are logged safely

**Tasks**
- [ ] Add structured logging for provider failures
- [ ] Log unresolved barcode outcomes
- [ ] Log authorization failures safely
- [ ] Add basic health-check strategy if included

### `CW-STORY-08.4` Seed realistic development data
**Release:** Post-MVP  
**Priority:** P2  
**Effort:** M
**User story:** As a developer, I want realistic sample data so workflows can be tested quickly during development.

**Acceptance criteria**
- Development-only seed data exists
- Seed data covers household, products, stores, and price history
- Production is not polluted with fake data

**Tasks**
- [ ] Seed one household
- [ ] Seed grocery concepts and categories
- [ ] Seed brands and products
- [ ] Seed product identifiers
- [ ] Seed store locations
- [ ] Seed purchase and price history
- [ ] Restrict seeding to development only

### `CW-STORY-08.5` Harden HTML assertions in Web integration tests
**Release:** MVP  
**Priority:** P2  
**Effort:** XS  
**User story:** As a developer, I want Web integration test assertions to be resilient to HTML encoding so tests don't produce false negatives on ordinary characters in test data.

**Acceptance criteria**
- Web integration test assertions against rendered HTML compare against HTML-decoded content, not raw encoded output
- Test data is not artificially restricted to avoid HTML-special characters (apostrophes, ampersands, quotes)

**Tasks**
- [x] Add an HTML-decoding assertion helper to `CartWise.Web.Tests`
- [x] Update `HouseholdAccessControlTests` to use realistic household names (including an apostrophe) via the new helper
- [x] Note the pattern so future integration tests use it by default

**Status:** Done — added `HtmlAssert.Contains`/`DoesNotContain` (`tests/CartWise.Web.Tests/HtmlAssert.cs`), which runs `WebUtility.HtmlDecode` before comparing. `HouseholdAccessControlTests` now uses `"Alice's Household"` / `"Bob's Household"` (real apostrophes) through the new helper and passes. Future Web integration tests asserting against rendered HTML should use `HtmlAssert` rather than raw `Assert.Contains`/`DoesNotContain`.

**Origin:** Discovered during `CW-STORY-02.4` verification — `EachUser_OnlySeesTheirOwnHousehold` failed against `"Alice's Household"` because Razor correctly HTML-encodes `'` as `&#x27;` in `@Model.Name`, but the test asserted a literal apostrophe. The immediate fix (switching to apostrophe-free test data) papered over the real issue: any test data with `&`, `'`, `"`, `<`, or `>` would hit the same false negative. This story is the real fix.

---

## Suggested 30-Day Delivery Sequence

### Week 1 - Foundation
- `CW-EPIC-01` Platform Foundation
- Start `CW-EPIC-02` Household and Access Control

### Week 2 - Shared List
- Finish `CW-EPIC-02` Household and Access Control
- Start and finish core stories in `CW-EPIC-03` Smart Shopping List

### Week 3 - Purchases and Price History
- Deliver `CW-EPIC-05` MVP stories

### Week 4 - Running Low and Hardening
- Deliver `CW-EPIC-07` MVP stories
- Deliver selected `CW-EPIC-08` hardening stories
- Run local MVP release checklist

### After MVP
- Resume `CW-EPIC-04` Product Catalog and Barcode Resolution
- Resume `CW-EPIC-06` Shopping Mode
- Complete remaining polish work in `CW-EPIC-08`

## Story Template

Use this format for new backlog items:

```md
### `CW-STORY-XX.X` Story Title
**User story:** As a `user type`, I want `capability` so that `benefit`.

**Acceptance criteria**
- Criterion 1
- Criterion 2

**Tasks**
- [ ] Task 1
- [ ] Task 2
```

## Notes

- Keep v1 scope tightly aligned to the grocery-memory goals
- Avoid introducing speculative architecture or out-of-scope features
- Reprioritize stories only when dependencies or product value clearly require it
- Preserve the implementation order unless a small change is needed to unblock progress
