The family app's Shopping screen: The Usuals strip and the last-bought timeline, run on real purchase history

The actual documents the agents read and work from, shown exactly as they are on disk — not a summary. See the progress view instead · All projects

Plan PLAN-PEARL-USUALS.md

# PLAN-PEARL-USUALS.md — THE USUALS: a strip of what the house buys again and again, and a last-bought timeline, added to the Pearl Shopping screen and running on the real check-off history

**🔴🔴 THIS IS THE ONLY PLANNING DOCUMENT FOR THIS SUBPROJECT. Do not create a second plan, tracker, summary, or scratch state file for it — extend THIS file or its STATE-PEARL-USUALS.md companion, and log a dated delta in PLAN-CHANGES-PEARL-USUALS.md. Any status view about this subproject is GENERATED from this plan and its state file; if a view disagrees with this plan, the plan wins.**

**Owner:** the Pearl Shopping lane (branch `pearl-shopping/drive`, worktree `.claude/worktrees/pearl-shopping`; any machine) · **Overseer:** Fable, one thread for the Pearl screens bucket (build work + design QA) · **Design authority:** Sienna (`creative-director`), UI only — taste graded ONLY after the fidelity count is zero · **Plan author:** Boris (`senior-engineer`)
**Rule: no step begins until its named entry artefact exists and its predecessor's PROOF has been produced and closed by a checker that is not the builder. A step with an unproven predecessor is a violation, not a shortcut.**

**Authority order:** Nick's dated words in §1a → this plan → `projects/personal/family-app/PLAN-PEARL-SHOPPING.md` (the CLOSED plan whose screen this extends: its §D block, anchor map, fidelity suite, evidence folder and step shapes are reused, never rebuilt) → `projects/personal/family-app/PEARL-DESIGN-SYSTEM.md` (the system) → `projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/PEARL-SCREEN-PROMPT.md` (the layout bar).

---

- **NORTH STAR:** Nick opens Shopping on his Mac or his phone at family.heroesandsidekicks.io and, above the store lists, sees THE USUALS — the things this house buys again and again, each with a bold-monoline drawing of the product, "last bought · N days ago" and "every N days", one tap putting one back on its usual store's list — and, on the desktop, the LAST BOUGHT timeline filling the right-hand column with the real dates the family checked things off. All of it runs on the household's REAL purchase history from the Shopping board, nothing invented, on desktop and one-handed on the phone. His words: "could we come up with a visual list of the things we're adding to the list or something- like photos on desktop of the products that we normally buy and a cool way to check them off" · "so far i like th concept of the usuals - good idea" · "this section must be wired in as its new" (all 2026-09-06).
- **FINISH LINE** (written now; the bar never rises mid-drive — anything found after these pass goes on the NEXT list):
  1. `family.heroesandsidekicks.io/#shopping`, signed in as Nick (Pearl is the default skin since deck-family-v593), renders the usuals strip and, at 1280×662, the last-bought timeline, with the existing store lists, Add bar and check-off unchanged.
  2. Shopping (extended) — `mismatched properties: 0 · unmeasured anchors: 0` at 375×812 light · 1280×662 light, across the closed plan's anchors #1–#43, #55 AND this plan's new anchors #56 onward (including the sibling-ORDER anchors for the strip and the timeline) — the suite run as `--only header,toolbar,lists,layout,usuals,timeline`; the chrome group #44–#54 (the phone tab bar and the desktop menu) is the Home lane's, measured and printed but never fixed or counted here, by Sienna's 2026-09-06 ruling and the suite's own group note — on the published URL, reproduced by a checker who did not build it (STEP 14).
  3. The history is PROVEN against the board: STEP 1's ground-truth file records the count of Done items read through the read-only proxy, the date range they span, and the derived usuals; a Sonnet checker recounts the Done items on the board and gets the same count; the usuals on the live screen are exactly the derived list (STEP 15 compares them).
  4. Every usual shows a drawn bold-monoline asset from the sprite, chosen by the keyword table; no tile is ever blank (STEP 12's count: tiles = drawn assets, pictures = 0).
  5. The phone strip is driven one-handed: a touch swipe scrolls it, every tap target on it is at least 44 px tall, and the page never scrolls sideways at 375 (STEP 13).
  6. The four-item publish gate has landed (STEP 14): the two zero-count lines, the checker's side-by-side PNGs at both widths, Sienna's taste verdict, the verifier's click-through — and the anonymous `/api/finances` request still returns 401 after the publish.
  7. Every §2 row is verified on the live surface by the blind checker (STEP 15), and the postmortem is written (STEP 16).
- **NEXT list** (found after the finish line, never worked in this drive): the one-store-at-a-time phone mode from The Till Roll concept (§1a row 7 — Nick, 2026-09-06: "one at a time - we can try later") · the last-bought timeline on the phone (§1a row 5, unless Nick says yes before STEP 2 closes) · real product photos on linked usuals through a cached image route (the retired STEP 6 + the photo half of STEP 12, restorable in one round — Nick's icon answer was bold monoline alone).

---

> **STEP 0 — ARM THE LOOP, BEFORE ANYTHING ELSE.** Set a 5-minute loop. Every time it fires, answer these four in order and CORRECT any failure before doing anything else:
> 1. **NORTH STAR** — is what I am doing this minute moving this plan's North Star? If not, drop it and take the highest-value unblocked step that does.
> 2. **FAN-OUT** — is my queue full up to the concurrency cap (§T)? Full capacity means the cap is reached and a queue of ready work sits behind it — NEVER "launch everything at once". Below the cap with ready work → dispatch now. At the cap → queue, don't launch.
> 3. **CHEAP** — are cheap models doing the building? If anything expensive is building, move that work down now.
> 4. **BLOCKED** — for anything I have called blocked: name the three concrete things I tried. If I cannot, it is not blocked — drive through it now.
> Then keep building. The loop never stops until the FINISH LINE is proven.

---

## Already true (the distilled past — facts, not story)

- The Pearl Shopping screen is CLOSED and live (14/14 steps, deck-family-v640 and every peer publish since; design REV 10.11 Mint; `css/pearl-shopping.css` v4, `js/pearl-shopping.js` v4) — evidence: `projects/personal/family-app/PLAN-PEARL-SHOPPING.md` STEPS, `projects/personal/family-app/STATE-PEARL-SHOPPING.md` resume snapshot, `projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/evidence/shopping-publish-round3.md`.
- Pearl is the app's DEFAULT skin: `js/pearl-nav.js` v8 adds `body.skin-pearl` unless `?skin=field` (measured 2026-09-06 08:00Z, PLAN-CHANGES-PEARL-SHOPPING.md 2026-09-06 delta) — so the everyday app WITH PEARL OFF is reached only at `?skin=field`, which is the URL every fence proof in this plan targets, and anything published here is live for Nick and Chantelle the moment it lands.
- The concept Nick picked exists as a rendered page in the lane's branch: `redesign-mockups/concepts-2026-09-06-shopping-ideas/idea-4-the-usuals.html` (live at https://skippy-designs.pages.dev/family-pearl-shopping-idea-4-the-usuals.html), with the strip (`.ustrip > .utile`, lines 92–104), the timeline (`.memory > .mrow`, lines 136–145) and the phone rules (lines 147–167). The six-style icon sheet is `redesign-mockups/concepts-2026-09-06-shopping-ideas/usuals-icon-styles.html` (styles 01–06; 02 "bold monoline", lines 68–70, is the builder's recommendation and this plan's default). Both are CONCEPTS, not the locked target — STEP 2 locks it.
- The Shopping feed and its hooks are known and frozen: board 18420185357, columns `color_mm4vj466` Status · `dropdown_mm4vv28h` Category (multi-select) · `text_mm4v2h9g` Quantity · `link_mm57q076` Buy link (`projects/personal/family-app/js/app.js` lines 2018–2024); the client query is one `items_page(limit:100)` with Done items filtered out client-side (lines 2142, 2160–2171); item shape `{id, name, category, qty, link}`; each rendered row is `.shop-prio[data-id]` with its name text in `.pt`, inside its store's `.shop-panel` (`#shop-iherb #shop-amazon #shop-walmart #shop-costco #shop-pharmacy #shop-unsorted`; `renderShoppingList`, line 2339 — the closed plan's frozen hook list); `loadShopping()` is a top-level function declaration in a classic script (line 2132) and `SHOP_ITEMS_BY_PANEL` a top-level `let` (line 2426); check-off is `onShoppingCheck` (lines 2476–2503) posting `/api/shopping-complete`; the empty string `List's empty.` (line 2497).
- The server side is three narrow functions: `projects/personal/family-app/functions/api/shopping.js` is a read-only Monday GraphQL passthrough that refuses any `mutation` (line 27) and signs with `MONDAY_API_TOKEN` (line 36); `projects/personal/family-app/functions/api/shopping-complete.js` runs ONE `change_column_value` setting Status to "Done" (lines 26–34) and writes no date; `projects/personal/family-app/functions/api/shopping-add.js` creates an item from `{name, store, qty?}` with `store` one of `iherb · amazon · walmart · costco · pharmacy` (lines 2, 51–57, 107–109) and requires a signed-in human or the robot credential (lines 88–98).
- NO history endpoint, job or cache exists (searched two ways 2026-09-06: `ls` of `functions/api` — six shopping files, none returning Done items with dates; `command grep -rn 'updated_at\|activity_logs\|shop-img\|link-image\|og:image'` over `functions/` and `js/` — zero lines). The read-only proxy will serve any READ query, so a Done-filtered `items_page` with `updated_at`, pagination past 100, and the board's `activity_logs` for the Status column need no server change.
- Images: nothing fetches a product picture anywhere (the grep above); the KV namespace `FAMILY_FINANCES` is bound and read by `projects/personal/family-app/functions/api/shopping-bundles.js` (lines 17, 21); `projects/personal/family-app/functions/api/ollie-img.js` (lines 11–49) is the KV blob-serve pattern (`ollie-img:<k>` → `{b64, mime}` → bytes with the stored content-type); R2 is not bound; no function uses Cloudflare image resizing (`cf: {image` — zero lines).
- The locked-target machinery exists and is shared: generator `projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/gen.mjs` (REV chain in its line-1 header, currently ending at REV 12 for the To-Do lane; the generator run for one screen writes `shopping.html`; a screen module may read its own pull — precedent `screens/todo.mjs` at REV 11; `extraFrames` draws extra states, lines 193–205) + `projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/screens/shopping.mjs` (80 lines, `compose(ctx)` → `{phone, desk, extraCss}`); published copy `projects/personal/skippy-app/design-directions/family-pearl-shopping-r10.html` deployed by `node projects/ops/deploy.mjs skippy-designs` (`projects/ops/deploy.mjs` lines 301–302) to https://skippy-designs.pages.dev/family-pearl-shopping-r10.html.
- The fidelity suite exists and is the ONE suite: `projects/personal/family-app/tools/pearl-fidelity-shopping.mjs` (171 KB — the cheap typist refuses it by label, so its edits are applied on Anthropic with the identical proof, recorded), flags counted in its source 2026-09-06 (`--selftest --only --inject --inject-js --out --shot --fence-only --fence-pair --fence-skin-diff --states --states-red --drive --band --chrome --map --target-file --as --skin --retired …`), the 55-row anchor map embedded from line 126 (its group note at lines 135–141) with groups `header · toolbar · lists · layout · chrome`, and `--map` overriding the map file. The signed map is `projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/gen.mjs-anchors-shopping.md` (55 anchors, 0 gaps; #50–#53 ruled Home-lane chrome by Sienna 2026-09-06 14:15Z).
- The evidence folder is `projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/evidence/` (459 files on disk in the worktree after the 2026-09-06 rebase); this plan's files are named `usuals-*`.
- Build and deploy: `node projects/personal/family-app/build-dist.js`, the parity gate `node projects/ops/skippy-jobs/_test-family-app-asset-version-parity.mjs`, `node projects/ops/deploy.mjs deck-family` (never a hand-rolled wrangler; it refuses a tree behind origin/main and clears dist/ first); `sw.js` declares `const CACHE` at line 408 (`deck-family-v647` when re-read 2026-09-06 after the rebase — re-measure with `command grep -n '^const CACHE' projects/personal/family-app/sw.js`).
- A Monday API token exists in the family vault under the entry name `monday-api-token` (listed by name only, 2026-09-06, `python3 projects/personal/family-vault/vault.py list --caller=skippy`) — so an agent can add the board's Date column itself at run time; Nick is never handed that task.
- Nick's standing grants cover every test here: `node projects/ops/skippy-jobs/lib/standing-auth.mjs --audit` (read 2026-09-06: agent-runs-its-own-tests · drive-nicks-machine-and-apps · click-through-identity-gate). No step asks him for any of these.
- The worktree is rebased onto origin/main (2026-09-06, `git rev-list --left-right --count HEAD...origin/main` → `2 0` after the rebase), 6 live sessions on the machine (`ls /tmp/cc-socks/ | wc -l` → 6), so this lane's concurrency cap is 6 (§5).

## 0 · Gate Zero receipts (the plan may not exist without these)
- Failure Mode Registry loaded: 2026-09-06, 189 entries (counted by the same row rule `check_plan.py` uses — `|`-rows minus separators minus `| Failure mode` headers — over `.claude/skills/plan/references/failure-registry.md`, which includes the sibling-ORDER row appended 2026-09-06); all 189 covered in §4, plus four NOVEL rows.
- Canonical specs loaded: `.claude/skills/plan/SKILL.md` (§N, §L, §G, §S, §T, §W, §Z, §AUTH, §BLOCKED, §A, §D, §B, Gate Zero), `.claude/skills/plan/references/plan-template.md`, `projects/ops/agents/DESIGN-FIDELITY-STANDARD.md`, `projects/ops/PROMPT-SPEC.md` P1–P7, `projects/personal/family-app/PLAN-PEARL-SHOPPING.md` (the estate), `projects/personal/family-app/PEARL-DESIGN-SYSTEM.md`, `projects/ops/agents/CODE-STANDARD.md`, `projects/ops/walkaway/MODEL-MATRIX.md`.
- Ownership check: this is a NEW SECTION of an EXISTING screen. The screen's estate is `projects/personal/family-app/PLAN-PEARL-SHOPPING.md` (closed 2026-09-06 17:35Z; its own STATE file names Nick's pick of the usuals concept as "a new plan; this one stays closed"), inside the family app's estate (`projects/personal/family-app/BUILD-PLAN-2026-08-17.md` + `projects/personal/family-app/PROJECT.md`). The ownership registry `projects/ops/spine-projections/ownership-injection.md` carries the family app at line 59 (`**Family app** · projects/personal/family-app/`) and no row for a shopping history, a usuals feed or a product-image cache (`command grep -n -i 'shopping' projects/ops/spine-projections/ownership-injection.md` → no lines, 2026-09-06). Searched two more ways: `ls projects/personal/family-app | command grep -c PLAN-PEARL-USUALS` → 0; `command grep -rl -i 'usuals' projects/ops --include='PLAN*.md'` → none. `ACTIVE-WORK.md` has no family-app row (`command grep -n -i 'family-app' ACTIVE-WORK.md` → no lines). So no plan, feed, store or cache for this exists; this plan is that subproject's own plan and it EXTENDS the closed Shopping plan's generator, anchor map, suite and evidence folder — never a second generator, suite, or image store (the KV namespace already bound is reused under a new key prefix).
- Expected inputs confirmed to exist: opened or listed on disk in the worktree 2026-09-06 — `gen.mjs`, `screens/shopping.mjs`, `shopping.html`, `gen.mjs-anchors-shopping.md`, `ground-truth-shopping.json`, `tools/pearl-fidelity-shopping.mjs`, `tools/pearl-accent-sites-shopping.json`, `css/pearl-shopping.css`, `js/pearl-shopping.js`, `index.html` (`#view-shopping` line 2666, `#pp-hero-shop` 2669, `#shopAddForm` 2682, `#shop-iherb` 2689, `#shop-unsorted` 2745), `sw.js`, `contract-check.js`, `js/app.js`, `functions/api/shopping.js`, `functions/api/shopping-complete.js`, `functions/api/shopping-add.js`, `functions/api/shopping-bundles.js`, `functions/api/ollie-img.js`, `idea-4-the-usuals.html`, `usuals-icon-styles.html`, `projects/ops/deploy.mjs`, `projects/ops/skippy-jobs/_test-family-app-asset-version-parity.mjs`, `projects/ops/skippy-jobs/lib/unified-project-update.mjs`, `projects/ops/agents/render_sheet.py`, `projects/ops/agents/check_plan.py`, `projects/shared-tooling/browser.mjs`, `projects/personal/family-vault/vault.py` (the gate password and the Monday token are read from it at run time by name and never written anywhere). Access: the read-only proxy needs a signed-in session (the identity gate, standing grant); the KV binding is live on `deck-family`; the Cloudflare Image Resizing feature is NOT confirmed — STEP 6 measures it and never assumes it.
- Model matrix: executors named from `projects/ops/walkaway/MODEL-MATRIX.md` vocabulary — glm (zai) and deepseek build through `projects/ops/cheap-task.mjs` (new files) and `projects/ops/route-build.mjs` (one existing file); sonnet checks and authors tests; opus builds ONLY where the cheap typist refuses (the 171 KB suite by label; `index.html` 632 KB and `sw.js` 178 KB by size; a step that must hold a credential at run time — the data floor), under `NICK-ASKED: opus — "pull back to opus and sonnet where reasonable for build" (Nick, 2026-09-05)`; fable oversees and Sienna design-QAs (Nick, 2026-09-05: Fable for planning/oversight only).
- Step numbering note (a deviation from the overseer's brief, with its reason): the brief numbered "lock the target" as STEP 1 and "ground truth of the history" as STEP 2. Here they are the other way round, because the drawing's usuals are REAL derived names and dates (the drawing invents nothing — `screens/shopping.mjs` line 4), so the drawing reads the ground-truth file, and `check_plan.py`'s CREATED-BY rule only exempts a file built by an EARLIER step. Nick's approval of the published drawing is still the ONE sanctioned wait (STEP 2.9).
- PLAN AUTHOR: Boris (`senior-engineer`, Fable), 2026-09-06, on the overseer's brief `usuals-plan-brief.md` written from Nick's three quoted asks; the plan-skill order Nick set 2026-09-05: "plans are written by three Fable agents: Sienna designs, Boris writes the plan, a cold verifier checks it".
- COLD READER: spec-breaker, 2026-09-06 (a different session, briefed only with this plan's path) — verdict NOT READY, 15 blocking + 10 non-blocking findings; all 25 applied the same day: the deploy loop closed (STEP 14 is the ONLY deploy; STEPS 5, 6 and 12 close on unit/injected proofs and their live halves run inside STEP 14.7) · the main-push contradiction resolved (main moves at STEP 14.1 only) · the count's anchor set made reproducible (`--only header,toolbar,lists,layout,usuals,timeline`; the chrome group #44–#54 measured, never counted) · STEP 8 now creates its two stubs first and proves the three assets exist in dist · test items carry a per-run random suffix so they can never reach the ≥ 2 rule · the sp-sec line filed at STEP 6, not STEP 16 · U13 reads min(8, M) · the derive test compares the client against STEP 1's independently-saved list and STEP 1's committed query · the activity log is paged and multiple Done events per item defined · `.pl-rail` (the navigation menu) disambiguated from the usuals rail · the resize option always passed and measured at STEP 14.7 · strings asserted K/K from the `strings` block · the store vote's first-label and tie-break rules · `.shop-prio` added to Already true · seven states in six frames, the coverage table U1–U18 with U19 measured not drawn · a differing icon pick re-locks the drawing under the REDLINE RULE · "bought once" defined on distinct dates · STEP 2.9 hands Nick the rendered sheet · the STEPS proof lines carry their inject flags and no placeholder · STEP 3's row floor 81 with a runnable count · the GAP wording · the suite's flag list completed · the SKILL.md section letters explained · the byte-for-byte overreach narrowed to the fence · the anchor-map line citation corrected · the `?skin=field` sentence rewritten. Re-checked: `python3 projects/ops/agents/check_plan.py` PASS.
- PROMPT-SPEC scan (P1–P7): kickoff mode, all seven rows walked — **P1** "the usuals" is undefined as a data rule (which items count, how many, how "last bought" is dated) — two readings give two data models, so the rule is fixed in §3 contracts (Done ≥ 2 times by normalised name; the date is the Bought-on column, else the Status activity-log event, else `updated_at`) and the count is §1a row 4. **P2** "this section must be wired in as its new" — the verb is BUILD ON REAL DATA: the strip and timeline read the board's history, never a hand-typed list (FINISH LINE 3). **P3** the brief's own negative "no history endpoint exists" was re-checked by grep and `ls` (Already true) — true. **P4** "photos on desktop of the products that we normally buy" — bounded by the usuals themselves; the one exclusion named: items without a link get a drawn asset, never a fetched guess (§1a row 3). **P5** landing spot: the pictures land in KV under `shop-img:<itemId>` and the plan's outputs in the evidence folder; nothing is homeless. **P6** "th concept", "sesktop", "hyperrcreative", "buynch" read as the / desktop / hyper-creative / bunch; "check them off" in his first ask is the app's existing check-off, not a new control. **P7** his third message bundles a redline (icons weak), a request (show other styles), a decision (build this version) and a scope rule (wired in) — split into §1a rows 2 and 1 and the North Star; the icon pick is on the sheet. Security work: none in this plan (§S) — the one security-shaped item (a server-side outbound fetcher, STEP 6) is filed as one line to `projects/ops/sp-sec/PLAN.md` at STEP 6, the moment the route is written, for the end-phase pass — never worked here. The anonymous `/api/finances` 401 after a publish (FINISH LINE 6, STEP 14.2) is the deploy tool's own documented post-publish check inherited from the closed plan, not security work.
- Section letters used in this plan (§N, §L, §G, §S, §T, §W, §Z, §AUTH, §BLOCKED, §A, §D, §B) refer to `.claude/skills/plan/SKILL.md`'s sections of those names; the §T dispatch header is copied into §5 of this plan so a builder never has to open the skill to dispatch.
- Hook notes for builders (house facts): the Bash routing hook refuses `cd` + a relative write target and `$VAR` write targets — use absolute-path script files; the cheap lane refuses briefs that list folders, point at `.md` files, or contain the word that trips its secret filter (it has tripped on a parameter NAME) — briefs name files by repo-relative path only and say "credential" rather than the tripping word; a `python3 -` heredoc opens files with literal quoted paths (the workspace path has a space); prose never goes through an unquoted shell heredoc.

## 1 · Goal and definition of done
> The drawing is CANONICAL once STEP 2 locks it: `projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/shopping.html` at the revision STEP 2 records. This plan points at it and never restates it in different words.

- **What we're building, one paragraph.** Under the Pearl skin, the Shopping screen gains two new things that run on the board's real check-off history: THE USUALS — a rail of tiles across the top of the content area on desktop (below the toolbar pill) and a horizontal thumb-scroll strip on the phone (below the Add card), each tile a bold-monoline drawing of the product from the sprite, the name, "last bought · N days ago", "every N days/weeks", and a "+ Add" that puts the item straight onto its usual store's list with a small landing motion, a dot marking tiles already on the list — and LAST BOUGHT, a dated vertical timeline in a third desktop column (most recent first, a serif footer line). One small data change makes future history exact: a "Bought on" Date column on the board that the existing check-off sets. Everything the closed Shopping plan shipped stays as it is, except the three wiring files STEP 8 touches once (a link tag, a script tag, three asset entries, the contract names); `js/app.js`, `js/pop.js`, `js/pearl-shopping.js` and `css/pearl-shopping.css` are edited zero times.
- **HOW IT'S USED:** Nick (Chantelle equal) opens Shopping at the desk or one-handed in the store, sees the eight things the house always buys, taps one to put it back on the list, glances at the timeline to see when things were last bought, and carries on with the lists as today. · HOW WE KNOW: his 2026-09-06 words in the North Star ("photos on desktop of the products that we normally buy… consider the fact that the mobile version needs to be optimized for shopping on the go") and the concept page he picked.
- **WHAT IT LOOKS LIKE:** exactly the locked drawing — the round-10 Shopping page extended: phone 375 with the strip under the Add card, desktop 1280×662 with the rail under the toolbar and the timeline as a 236 px third column, every state drawn — seven states in six extra frames (loading skeleton tiles · empty history · a tile with no link and a tile whose picture request failed, together on one frame · a tile already on the list · the phone strip mid-scroll · a timeline with fewer than three purchases), accent Mint `#1B8259 / #D8F0E4 / #146646` (Nick's redline 2026-09-06), light only. · HOW WE KNOW: Nick's approving words on the published page, recorded in §2d at STEP 2.
- **WHERE IT LIVES:** `https://family.heroesandsidekicks.io/#shopping` (production, Cloudflare Pages project `deck-family`), opened by Nick and Chantelle. · HOW WE KNOW: `projects/ops/deploy.mjs` names the project; Pearl is the default skin (Already true); Nick, 2026-09-06, on the LIVE Shopping screen: "this screen looks good as specced - now that its live its just a sea of negative space".
- **WHAT IT MUST DO:** (1) read the board's Done items through the existing read-only proxy, group them by normalised name, and derive the usuals (name · count · last bought · median interval · usual store) — the same derivation in STEP 1's pull and in the client, proven equal; (2) render the strip and the timeline from that derivation with zero invented strings, at both widths, matching the locked drawing at zero mismatches including sibling order; (3) put a tapped usual straight onto its usual store's list through the existing `/api/shopping-add`, mark the tile "On the list", and refresh the lists through the app's own loader; (4) draw a bold-monoline asset from the sprite on every usual, chosen by the name-keyword table, `p-cart` when no keyword matches — never a blank tile; (5) write "Bought on" at every check-off from now on, so future history is exact, with the existing confirm and the existing Done write unchanged; (6) work one-handed on the phone: swipe-scroll strip, 44 px targets, no sideways page scroll; (7) leave every other screen, the `?skin=field` screen, and the closed plan's anchors unchanged. Each is an eval in §6. · HOW WE KNOW: §2's rows and the suite's new modes (STEP 4).
- **NOT in scope:** the desktop rail and phone tab bar (`css/pearl-nav.css`, `js/pearl-nav.js` — the Home lane's; Sienna's 2026-09-06 ruling keeps anchors #50–#53 out of this screen's count) · any reorder or restyle of the existing store lists, rows, toolbar or header (closed and measured; the strip and timeline are ADDED around them) · the Order via Skippy button (never clicked in tests — ruled 2026-09-06 15:00Z: no test queue exists and it stages a real order card) · the check-off confirm label (the app's own composition, `js/app.js` line 2483) · the floating Ask Skippy launcher (hidden app-wide on Nick's order, never re-shown) · a second generator, suite, anchor map or image store · real product photos and any server-side image route (RETIRED 2026-09-06 — NEXT list; PLAN-CHANGES-PEARL-USUALS.md) · the one-store-at-a-time phone mode (The Till Roll — NEXT list unless Nick says yes) · the timeline on the phone (NEXT list unless Nick says yes) · security work of any kind (§S; the outbound fetcher goes to the end-phase pass as one line) · dark mode (Nick: "3 skip it", 2026-09-04).
- **REPLACING/RETIRING:** nothing. No existing component is retired; the concept pages under `concepts-2026-09-06-shopping-ideas/` stay as concepts and are named REJECTED-FOR-MEASUREMENT in every brief (ideas 1, 2, 3, 5 and the icon sheet's five unpicked styles): builders never open them "for reference"; the locked target is the round-10 `shopping.html` alone.
- **Trip-over protocol:** a builder that finds a defect outside the fence (a chrome mismatch, a Monday data oddity, a bug in the closed Shopping layer, a Finances-layer bug) writes ONE dated line to `STATE-PEARL-USUALS.md` under "Handoffs" naming the owner, then returns to its step — never investigates, never fixes.

Any inherited fact above carries its re-measure: hooks → `command grep -n 'id="view-shopping"\|id="shopAddForm"\|id="shop-unsorted"' projects/personal/family-app/index.html`; the cache version → `command grep -n '^const CACHE' projects/personal/family-app/sw.js`; the REV chain → `command grep -n '^// REV ' projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/gen.mjs`; the KV binding → `command grep -n 'FAMILY_FINANCES' projects/personal/family-app/functions/api/shopping-bundles.js`; the live session count → `ls /tmp/cc-socks/ | wc -l`.

## 1a · Critical variables — the confirmation sheet is GENERATED from this table

| # | The variable, in plain words | Value chosen | Alternatives rejected | Class | HOW WE KNOW | Cost if wrong | CONFIRMED |
|---|---|---|---|---|---|---|---|
| 1 | **SURFACE — which screen this lands on, and who opens it**: the live family app's Shopping screen at family.heroesandsidekicks.io, under Pearl (the default), as a new section above the store lists — opened by Nick (Chantelle equal) on desktop and phone | The live Shopping screen, extended in place | A separate "Usuals" screen or tab; a standalone page | V1 | Nick's own dated words on the live screen | Building a section nobody opens, or a tab he never asked for | Nick, 2026-09-06, "this screen looks good as specced - now that its live its just a sea of negative space - come up with some cool ways to make this feel more polished and useful" and "this section must be wired in as its new" · Nick 2026-09-06: "build for both now and ill test it live on real device" — desktop and phone both in this build; his own device look comes after the agents' proof, never as a test step |
| 2 | Which icon style from the six-style sheet draws the product pictures that have no photo | Bold monoline (style 02 on the sheet: ink line, one mint patch, no fetch, never a blank tile) | Styles 01 pencil, 03 flat duotone, 04 engraved, 05 watercolour, 06 monogram discs | V1 | His pick from the sheet he asked for | Every drawn tile in a style he finds "weak and out of sync with the rest of the branding" | CONFIRMED by Nick 2026-09-06: "bold monoline" — style 02 for every usual; the drawing's default already matches, so no republish is owed for this row |
| 3 | Whether real product photos are used for usuals that have a shop link (yes = the photo route is built; no = drawn icons only) | No — drawn bold-monoline icons for every usual; the photo route is NOT built in this plan (NEXT list) | A photo for every linked usual; photos only on desktop | V1 | His answer named a single style | Either a strip with no photos when he asked for them, or a server route he did not want | CONFIRMED by Nick 2026-09-06: his answer to the icon question was "bold monoline" alone, not the offered "1 plus 2" combination; photos are one word away and go on the NEXT list |
| 4 | How many usuals show — in the desktop rail and in the phone strip | Eight in both (the phone strip scrolls sideways past the first three) | Six; twelve; "all of them" | V1 | The concept he liked draws eight | A rail that wraps to a second row on desktop, or a strip too long to be a glance | DEFAULTED 2026-09-06 to eight (the concept's count, `idea-4-the-usuals.html` line 92); sheet row — Nick may override |
| 5 | Whether the last-bought timeline is wanted on the phone too, or desktop only | Desktop only (the phone gets the strip; the timeline would push the lists a screen down) | On the phone below the lists; on the phone above the lists | V1 | The concept draws it desktop-only and his phone rule is "optimized for shopping on the go" | A phone page a screen longer than it needs to be, or a timeline he wanted in his pocket and never got | DEFAULTED 2026-09-06 to desktop only (the concept's phone rule; a yes from Nick becomes its own NEXT-list step); sheet row — Nick may override |
| 6 | Whether a tap on "+ Add" puts the item straight on its usual store's list with no confirmation | Yes — straight on, with the tile turning "On the list" and the new row landing with a small motion; undo is the row's own check-off | A confirm dialog like check-off; a two-tap "Add · Really?" | V1 | The concept's tap behaviour he liked; the closed plan's Add form also confirms nothing | Either an accidental item on the list every time a thumb brushes a tile, or a confirm that makes the strip slower than typing | DEFAULTED 2026-09-06 to straight on (the concept, `idea-4-the-usuals.html` lines 377–396); sheet row — Nick may override |
| 7 | Whether the one-store-at-a-time phone mode from The Till Roll concept is folded into this build | Not in this plan — it goes on the NEXT list | Fold it in now as extra steps | V1 | He has not said; the build is smaller and ships sooner without it | Either a phone mode he wanted arriving a build later, or six extra steps on a build he wanted as "the usuals" | CONFIRMED by Nick 2026-09-06: "one at a time - we can try later" — NEXT list |

- V1 confirmation reads `<name>, <date>, "<their own words>"`; a DEFAULTED row carries its default's author, date and source and appears on the sheet — work downstream of it is a swap (a sprite, a count, a media rule), never a rebuild.
- V2 confirmation reads `opened <what>, <date>, saw: <what was actually there>`.

**Considered and ruled NOT critical** *(the denominator — never demote a variable silently)*:
- `How a name is normalised for grouping` — V2, opened the Done items at STEP 1; the rule is frozen in §3 and the checker recounts with it; two rules give two lists but Nick cannot settle it by preference.
- `How many purchases make a usual` — two or more Done occurrences by normalised name (§3); a single purchase is a one-off, not a usual; derived, not chosen.
- `Which date is "last bought"` — the Bought-on column when present, else the Status activity-log event, else `updated_at` (§3); STEP 1 measures which the board can supply today.
- `The picture byte cap and cache life` — 250 KB and 30 days, a mechanism choice inside STEP 6; the tile draws the icon when the cap is exceeded, so no reading changes what Nick sees except which tiles are photos.
- `Which cheap vendor builds` — the model matrix decides; the checker is what matters.
- `The exact cache version number` — derived at STEP 14 as max(main, live)+1, never typed.

## 1b · Subproject decomposition — could a piece of this ship on its own?

- **SINGLE SUBPROJECT:** this plan IS the subproject (the Usuals section of the Pearl Shopping screen). The Bought-on column alone changes nothing Nick sees; the image route alone serves pictures nowhere; the strip without the history is decoration he explicitly ruled out ("wired in as its new"). Its parent is the closed Shopping plan; its siblings (Finances, Home, Calendar, To-Do, Extras, Health) have their own plans and owners; the chrome is the Home lane's.

## 2 · The complete UX map (this becomes the test manifest verbatim)

| Id | Screen / entry point | State (default·empty·error·loading) | Element / interaction | Expected behavior | Navigation from → to |
|---|---|---|---|---|---|
| U1 | `/#shopping`, phone 375 | default | The usuals strip `.pl-usuals` under the Add card: a title row ("The usuals" · sub-line · "FROM THE CHECK-OFF HISTORY"), then `.pl-ustrip` scrolling sideways with N = §1a row 4 tiles | strip present, title strings as drawn, tiles = min(8, derived usuals count); DOM order of tiles = derived order (count desc, then last-bought desc) | any tab → Shopping |
| U2 | same | default | One tile `.pl-utile`: the drawn `svg use` from the sprite, name, "last bought · N days ago", "every N days" / "every N weeks", "+ Add" button | every value derived from the history; the interval line reads "bought once" when the group's purchases fall on fewer than two distinct dates (two check-offs on one day); every tap target ≥ 44 px tall | — |
| U3 | same | default | A tile whose usual is already on the open list (normalised-name match against the rendered `.shop-prio .pt` rows) | carries `.pl-on`: the dot, the button reads "On the list" and is inert (`aria-disabled="true"`) | — |
| U4 | same | default | Tap "+ Add" on a tile not on the list | POST `/api/shopping-add` `{name, store, qty:1}` with the usual's store; on 2xx the tile turns `.pl-on`, the app's own `loadShopping()` re-runs, the new row appears in that store's list with the landing motion class `.pl-landed` for one animation; the count in the header rises by one | — |
| U5 | same | error | Add fails (non-2xx or network) | the tile's button reads "Couldn't add — try again" for 4 s then returns to "+ Add"; nothing else changes; the string is the drawing's, verbatim | — |
| U6 | same | loading | Before the history query returns | eight skeleton tiles `.pl-utile.pl-usk` (no text, no button), same box as a real tile | — |
| U7 | same | empty | The board has no Done items, or none with two purchases | the strip is replaced by one quiet line `.pl-unone`: "No usuals yet — check things off and they'll show up here." (verbatim from the drawing); no tiles, no timeline | — |
| U8 | same | error | The history query returns non-2xx or the network drops it | the strip is hidden entirely (`hidden`); the lists render as today; one console line `[usuals] history unavailable` with the status; nothing else changes | — |
| U9 | same | default | A tile for any usual — every usual is drawn (§1a row 3) | the drawn asset from the bold-monoline sprite, chosen by the name-keyword table, `p-cart` when no keyword matches; no picture request is ever made | — |
| U10 | same | error | RETIRED 2026-09-06 with STEP 6 (no photo route in this plan) — a tile whose picture request returned 204 or whose `img` fired `error` | RETIRED 2026-09-06 with STEP 6: nothing ever requests a picture, so this state cannot be reached; the row is kept (§E) and is not counted in the blind check | — |
| U11 | same | default | The strip mid-scroll (touch swipe of 200 px leftwards) | `scrollLeft` > 0, the page's `scrollWidth <= clientWidth`, tiles stay 132 px wide, the strip's own scrollbar hidden | — |
| U12 | same | default | The timeline on the phone | absent (`.pl-memory` not rendered under 1100 px) — §1a row 5 | — |
| U13 | `/#shopping`, desktop 1280×662 | default | The usuals rail `.pl-usuals` under the toolbar pill, min(8, M) tiles in one row of an eight-column grid (`grid-template-columns: repeat(8, minmax(0,1fr))`; fewer than eight usuals leave the trailing cells empty, exactly as the drawing draws whatever M the ground truth yields), the title row with "FROM THE CHECK-OFF HISTORY" right-aligned | rail present between `.pl-shtb` and `.pl-cols2`; the frame still fits 662 with the lists scrolling inside their cards; sibling order toolbar → rail → columns | — |
| U14 | same | default | The last-bought timeline `.pl-memory` as the third column (`.pl-cols2` becomes `1fr 1fr 236px` at ≥1100 under this plan's scope): heading "LAST BOUGHT", one `.pl-mrow` per usual, most recent first, the two most recent dots filled accent, dates as "Sep 1", the footer line | rows = the usuals' count (≤ §1a row 4), DOM order = date descending, dates equal the derivation's last-bought dates; footer "Everything before this sits in the same history — Skippy just stops drawing it." verbatim | — |
| U15 | same | default | Timeline with fewer than three purchases in the whole history | the rows it has; the footer reads "That's the whole history so far." (verbatim from the drawing) | — |
| U16 | same | empty | Empty history on desktop | U7's line where the rail would be; the third column absent (`.pl-cols2` back to two columns) | — |
| U17 | same | default | Column heights | both list columns and the timeline column end at the menu's bottom (622) or short of it; nothing overflows the 662 frame; the timeline scrolls inside if taller | — |
| U18 | same | default | Tap "+ Add" on desktop (mouse) | U4's behaviour; the new row lands in the correct store card under the closed plan's STEP 9 column rule | — |
| U19 | `/#shopping`, 1024×768 | default | The 900–1099 band | phone layout: the strip, no rail, no timeline, `document.documentElement.scrollWidth <= clientWidth` | — |
| U20 | `/?skin=field#shopping`, 375 and 1280 | default | The everyday app with Pearl off | byte-identical DOM and computed styles to the pre-build capture — the suite's `--fence-pair off` prints `0 changed elements`; no `.pl-usuals`, no history query fired (network log count 0 for the Done-filtered query) | — |
| U21 | `/#shopping` as Chantelle, 375 and 1280 | default | Same screen signed in as the second identity | identical layout; the same usuals (same board); her tap adds through her own signed-in session | — |
| U22 | Check-off of any row (existing control), signed in as Nick | default | `onShoppingCheck` → `/api/shopping-complete` | the board item reads Status "Done" AND "Bought on" = today's date in America/Cancun (read back through the proxy by item id); the confirm text and the row's removal are exactly as today | — |
| U23 | `/api/link-image?item=<id>` directly, signed in | default · error | RETIRED 2026-09-06 with STEP 6 (no photo route in this plan) — the image route | RETIRED 2026-09-06 with STEP 6: the route is not built in this plan, so there is nothing to reach; the row is kept (§E) and is not counted in the blind check | — |

## 2d · DESIGN FIDELITY GATE (plan skill §D)
- **LOCKED TARGET:** generator `projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/gen.mjs` + `projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/screens/shopping.mjs` (extended by STEP 2 with the usuals rail, the timeline, the picked icon sprite and the seven drawn states as `extraFrames`, reading `ground-truth-usuals.json` for every name and date) → output `projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/shopping.html` (the single output; redlines go into the generator and it is republished) · published login-free address: `https://skippy-designs.pages.dev/family-pearl-shopping-r10.html` (the SAME address as the closed plan — a new REV, byte-identical copy in `projects/personal/skippy-app/design-directions/`) · Nick's approving words: **NOT YET LOCKED — STEP 2 builds it**; his yes on the PUBLISHED page at its named revision lands here as `Nick, <date>, "<words>"`, the one sanctioned wait in this plan; every step whose gate does not name "STEP 2's approved revision" runs meanwhile · approved revision: recorded by STEP 2 as `REV <n+1> · commit <hash>` where n is the last number in the generator's line-1 REV chain at run time (the generator is shared with the To-Do and Extras lanes; 12 when re-read 2026-09-06).
- **TARGET HASH:** machine-written by STEP 2 (`shasum -a 256` of the local output and of `curl -sL` of the published address, equal, in the same shell) into `STATE-PEARL-USUALS.md` and this line · **REJECTED DESIGNS (do not open):** `concepts-2026-09-06-shopping-ideas/idea-1-pantry-wall.html`, `idea-2-till-roll.html`, `idea-3-the-run.html`, `idea-5-contact-sheet.html`; the five unpicked styles on `usuals-icon-styles.html`; `idea-4-the-usuals.html` itself once STEP 2 has ported it (it is the CONCEPT, not the target — its demo strings and eight demo names are not data).
- **ANCHOR MAP:** `projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/gen.mjs-anchors-shopping.md` — the closed plan's signed 55-row map EXTENDED by STEP 3 with rows #56 onward (the strip, the title row, one tile of each kind, the picture, the drawn asset, the add button in both states, the dot, the skeleton tile, the empty line, the timeline, its heading, one row of each dot kind, the date, the name, the footer; and the sibling-ORDER anchors: `.pl-ustrip` children, `.pl-memory` children, the desktop content box's children toolbar → rail → columns, and the phone stack's children Add card → strip → lists), re-signed by Sienna with the drawing's new distinct-element count beside the new anchor count, hash machine-written — the rows are CREATED BY STEP 3 (the file exists today).
- **FIDELITY CHECK:** `projects/personal/family-app/tools/pearl-fidelity-shopping.mjs` — the ONE suite, extended by STEP 4 with groups `usuals` and `timeline`, an `--order` mode that prints each ordered container's DOM order and painted tops and fails on any difference from the design, `--usuals-states` forcing U6/U7/U8/U10/U15 through network blocking and response substitution, `--drive usuals` (the injected-tile add-and-remove), and `--usuals-phone` (the swipe measurement); selftest → `0 · 0`, sabotage → red naming every compared property AND the order mode red on a swapped pair — the modes are CREATED BY STEP 4 (the file exists today). The retired-colour sweep and the plain-app fence (`--fence-pair off`) are the closed plan's and run unchanged.
- **VIEWPORTS AND THEMES:** 375×812 light · 1280×662 light — equal to the locked target's own list (the drawing draws phone 375 and desktop 1280 at its 662 px frame; light only, dark skipped by Nick) — signed by Sienna at STEP 3 alongside the new anchors.
- **RULE:** no screen closes above `mismatched properties: 0 · unmeasured anchors: 0` at every viewport × theme, reproduced by a checker who did not build it, or with an unsigned GAP; more than two GAPs on a screen is a FAIL; an upstream dependency keeps a screen OPEN, never closes it. The count for THIS plan is over anchors #1–#43, #55 and #56 onward, produced by the suite run as `--only header,toolbar,lists,layout,usuals,timeline`; the chrome group #44–#54 (the phone tab bar and the desktop menu, the suite's own `chrome` group) stays the Home lane's — measured, printed, never fixed and never counted here — by Sienna's 2026-09-06 ruling on #50–#53 and the suite's group note "owned by the Home/chrome lane, measured not fixed".
- **REDLINE RULE:** STEPS 3–13 may run against the published-but-not-yet-approved page (the map and the suite address elements by selector, so a redline changes expected VALUES, not the build's hooks). If Nick redlines, STEP 2 republishes REV n+2, this block's revision line moves, Sienna re-signs only the anchors whose design changed, and STEPS 9–13 re-run their injected check against the new page — one check round, never a rebuild. STEP 14 opens only on the APPROVED revision.
- **GAPS:** none yet, and the closed plan's screen carries 0, so the ceiling of two is whole. Two candidates are named here so nobody meets them first at STEP 14; Sienna rules on each at STEP 3, and a ruling of "not a GAP" spends nothing: the loaded photo's pixels are not a computed style (the anchor measures the `img` box, `object-fit` and radius; the photo's content is graded on the side-by-side); the landing motion (U4) is a transient — measured by `--drive usuals` as a class present for one animation, not by the count.
- **FINISH LINE carries**, per screen: `Shopping (extended) — mismatched properties: 0 · unmeasured anchors: 0 at 375×812 light · 1280×662 light`; every publish step's PROOF carries the four §A items (the count, the checker's side-by-side PNGs, the creative director's verdict, the verifier's verdict).

## 3 · Lanes and frozen contracts

**File fences are drawn so no two lanes need the same file in the same hour (skill §W). Scoped commits only (`git commit -m "..." -- <paths>`, ending `Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>`); never `git stash`; never `git merge`; never `--no-verify`; re-read a file immediately before writing it; every step's first action is `git -C "/Users/nickdeck/Documents/Claude 2.0/.claude/worktrees/pearl-shopping" rev-parse --show-toplevel` and `git status --porcelain`, both recorded; the branch is pushed with `--force-with-lease`; main moves at ONE place only — STEP 14.1, by the publish rule the closed plan set (the branch is pushed to main before the deploy tool runs) — and at no other step.**

| Lane | Scope (in / out) | Owner | Definition of done | Model (explicit) |
|---|---|---|---|---|
| USUALS | In: NEW `css/pearl-usuals.css`, NEW `js/pearl-usuals.js`, NEW `img/usuals-icons.svg` (the picked style's sprite), NEW `tools/usuals-bought-on-column.mjs`, NEW `tools/usuals-pull-history.mjs`, NEW `tools/usuals-derive.test.mjs`, NEW `redesign-mockups/concepts-2026-09-04-round10-screens/ground-truth-usuals.json`, NEW `STATE-PEARL-USUALS.md`, NEW `PLAN-CHANGES-PEARL-USUALS.md`, evidence under the round-10 `evidence/` folder named `usuals-*`; EXTENDED (append-only rows/modes): `screens/shopping.mjs`, `gen.mjs` (line-1 REV header only), `gen.mjs-anchors-shopping.md`, `tools/pearl-fidelity-shopping.mjs`, `functions/api/shopping-complete.js` (one mutation widened); ONE edit each (STEP 8) to `index.html`, `sw.js`, `contract-check.js`. Out: `js/app.js`, `js/pop.js`, `js/pearl-shopping.js`, `css/pearl-shopping.css`, `css/pearl-shell.css` (the closed Shopping plan's, frozen), every `pearl-*` file the Finances/Home lanes own (`css/pearl.css`, `css/pearl-nav.css`, `js/pearl-nav.js`, `css/pearl-finances.css`, `js/pearl-finances.js`), `functions/api/shopping.js`, `functions/api/shopping-add.js`, `functions/api/shopping-order.js`, every other view. | this drive | FINISH LINE items 1–7 | glm builds via `projects/ops/cheap-task.mjs` / `projects/ops/route-build.mjs` · deepseek extracts · sonnet checks and authors tests · opus builds only where the typist refuses or a credential is held at run time (NICK-ASKED) · fable (Sienna) design-QAs and oversees |

**Contracts between lanes and inside this one (FROZEN at plan time — change = dated `PLAN-CHANGES-PEARL-USUALS.md` delta):**
- **The switch and the scope:** everything renders only under `body.skin-pearl` (added by `js/pearl-nav.js`); every rule in `css/pearl-usuals.css` is scoped `body.skin-pearl #view-shopping .pl-usuals` / `.pl-memory`; `js/pearl-usuals.js` returns immediately without `body.skin-pearl` or without `#view-shopping`. The accent is inherited from the closed plan's scope tokens (`--acc:#1B8259;--soft:#D8F0E4;--deep:#146646`, set on `body.skin-pearl #view-shopping` by `css/pearl-shopping.css`) — never re-declared.
- **THE HISTORY RULE (the P1 answer, stated once, cited everywhere):** (a) the source is the board's Done items, read through `POST /api/shopping` with `items_page(limit:500, query_params:{rules:[{column_id:"color_mm4vj466", compare_value:[DONE_INDEX], operator:any_of}]})` and `next_items_page` by cursor until the cursor is null, where DONE_INDEX is the index whose label is "Done" in the Status column's `settings_str` (read in the same query, never typed); (b) `normalise(name)` = lower-case · trim · collapse whitespace · drop any parenthesised part and anything after " — " · drop trailing punctuation; (c) a group is one normalised name; a USUAL is a group with ≥ 2 Done items; (d) each Done item's DATE is the "Bought on" column when set (STEP 5), else the Status activity-log event that set it to "Done" (`activity_logs(column_ids:["color_mm4vj466"])`, read to exhaustion by page, `created_at` ÷ 10 000 → ms) when the board returns one, else `updated_at`; an item with several Done events contributes each distinct Done date as a purchase; STEP 1 records which source each item used; (e) last bought = the group's max date; interval = the median of the gaps in days between consecutive dates (fewer than two dates → "bought once"); the interval line reads "every N days" for N < 14 and "every N weeks" otherwise (N rounded); (f) the usual's STORE = the most frequent FIRST label across the group's items (the first comma-separated label of the Category column's `text`, in the order Monday returns it), ties broken by the switcher's order iHerb · Amazon · Walmart/Local · Costco · Pharmacy, mapped by the app's own `SHOPPING_STORES` keys to `/api/shopping-add`'s `store` (`iherb · amazon · walmart · costco · pharmacy`); a group whose store maps to none is shown with no "+ Add" (the chip reads "no store yet") and never added; (g) ordering = count desc, then last-bought desc; the strip shows the first N (§1a row 4), the timeline shows the same N ordered by last-bought desc; (h) "on the list" = normalised name equals the normalised text of any rendered `.shop-prio .pt` row. The SAME function implements (b)–(h) in `tools/usuals-pull-history.mjs` (STEP 1) and `js/pearl-usuals.js` (STEP 7): the derive function lives once in `js/pearl-usuals.js` under `globalThis.plUsualsDerive` and the pull tool imports that file; `tools/usuals-derive.test.mjs` proves both call the same bytes.
- **The picture contract:** RETIRED 2026-09-06 (no photo route in this plan — Nick's icon answer was "bold monoline" alone; PLAN-CHANGES-PEARL-USUALS.md). Every tile draws its asset from the sprite; the drawn-asset sprite contract below is the live one.
- **The Bought-on contract:** a Date column named "Bought on" on board 18420185357, created ONCE by `tools/usuals-bought-on-column.mjs` (idempotent: it reads the board's columns first and creates only when no column titled "Bought on" exists), its id recorded in `STATE-PEARL-USUALS.md` and as `BOUGHT_ON_COLUMN_ID` in `functions/api/shopping-complete.js` and `js/pearl-usuals.js`; the check-off mutation becomes ONE `change_multiple_column_values` carrying `{ [STATUS]: {label:"Done"}, [BOUGHT_ON]: {date: "YYYY-MM-DD"} }` with the date computed server-side in `America/Cancun`; every other line of `shopping-complete.js` stays byte-identical (the checker diffs).
- **The hook rule (Finances spec §6.4, inherited):** `js/app.js`, `js/pop.js`, `js/pearl-shopping.js` and `css/pearl-shopping.css` are edited zero times; `js/pearl-usuals.js` inserts its nodes on a `MutationObserver` over `#view-shopping` with a re-entrancy guard and a microtask debounce (the same mechanism as `js/pearl-shopping.js`), after the closed plan's takeover has composed the toolbar (`.pl-shtb` present) — the rail is inserted as the next sibling of `.pl-shtb` at ≥1100 and as the next sibling of `.shop-add-card` below; the timeline is inserted as the last child of `.pl-cols2` and the grid widened by ONE rule in `css/pearl-usuals.css` (`body.skin-pearl #view-shopping .pl-cols2:has(> .pl-memory){grid-template-columns:1fr 1fr 236px}`), so the closed plan's column rule is untouched. The app's refetch after an add is `loadShopping()` called by bare name when `typeof loadShopping === "function"`, else a `#shopAddForm` submit is NOT faked — the tile reports the U5 string.
- **Words:** "the rail" in this plan is `.pl-usuals` in its desktop layout (the concept's word); `.pl-rail` is the desktop navigation MENU (the Home lane's node) and is only ever asserted absent at 1024. "The strip" is the same `.pl-usuals` in its phone layout.
- **Neutraliser:** explicit, enumerated properties only inside the scope; `all: unset` and `all: revert-layer` are banned.
- **Widths:** 375 · 1024 · 1280; breakpoint 1100 px; light only.
- **Accent sites (additions to `tools/pearl-accent-sites-shopping.json`, appended by STEP 9):** `.pl-utile .pl-uadd` (text + hairline), `.pl-utile.pl-on .pl-uadd` (filled), `.pl-utile .pl-udue`, `.pl-mrow.pl-recent::before`; the accent may never appear on a label, heading, body text, hairline or card edge.
- **The drawn-asset sprite:** `img/usuals-icons.svg` carries one `<symbol id="p-…">` per product class in the picked style plus `p-cart`; the keyword table (name → symbol) lives once in `js/pearl-usuals.js` and STEP 2's drawing reads the same table from `ground-truth-usuals.json`'s `symbols` block, so drawing and build cannot disagree.
- **Evidence and state:** artefacts under the round-10 `evidence/` folder as `usuals-step<N>-<what>.<ext>`; `STATE-PEARL-USUALS.md` is current-state only, rewritten in place; `PLAN-CHANGES-PEARL-USUALS.md` line 1 is the cold reader's verdict.

## 3b · Execution map — the Step map, then one STEP block per row

A task is DONE only when its review-ledger row is CLOSED by a reviewer that is not the builder.

**Standing rules for EVERY step (inherited from the closed plan, restated once):** (1) first action `git -C "/Users/nickdeck/Documents/Claude 2.0/.claude/worktrees/pearl-shopping" rev-parse --show-toplevel` and `git status --porcelain`, both recorded; (2) snapshot before edit, step-numbered: `<file>.pre-pearl-usuals-step<N>-<date>.bak`, deleted only after the step's checker closes it; (3) commit with an explicit pathspec, then `git show --stat HEAD`, then `git merge-base --is-ancestor <hash> HEAD` at every citation; (4) any step touching `index.html` tags also edits `sw.js` (ASSETS + CACHE) in the same change; (5) every step ends with `node projects/personal/family-app/contract-check.js` green with the number quoted and `node --check` on every touched `.js`/`.mjs`; (6) builder = the cheap lane with the §T dispatch header pasted verbatim; where the typist refuses (`tools/pearl-fidelity-shopping.mjs` by label; `index.html`/`sw.js` by size; a run that must hold a credential), the SAME byte-exact hunk is applied on opus under `NICK-ASKED: opus — "pull back to opus and sonnet where reasonable for build" (Nick, 2026-09-05)` with the identical proof and the refusal recorded in the state file; checker = a Sonnet session that did not build it, dispatched by the builder the moment the proof exists; (7) every `--prove` states what must NOT change and proves it; (8) no proof hardcodes a count it could derive; (9) every step close runs `node projects/ops/skippy-jobs/lib/unified-project-update.mjs` for this plan so the STEPS line, the board and the status page move on one beat; (10) audit means RE-RUN the proof, never recite last-known status.

**Step map (read this first):**

| Stage | # | Task (step name) | Gate to enter | EXECUTOR (model, from the matrix) | CHECKER (different model — never the builder) | DONE-PROOF (runnable command) | Ends when |
|---|---|---|---|---|---|---|---|
| Plan | 1 | Ground truth of the history: Done items through the read-only proxy, dates, grouping, the derived usuals — printed, saved, recounted against the board | nothing — start now | opus (NICK-ASKED; the pull holds the gate credential at run time — data floor) | sonnet | `node tools/usuals-pull-history.mjs --check` (CREATED BY STEP 1's run; the checker re-runs it) prints `done items: N · date range: A → B · usuals: M · sources: bought-on X / activity Y / updated_at Z` and `ls projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens` shows `ground-truth-usuals.json` | the checker's own recount of Done items equals N and its own derivation equals the M names in order |
| Plan | 2 | Lock the target: extend the drawing with the rail, the timeline, the picked sprite and seven drawn states; new REV at the same address; open the state files; Nick's words | STEP 1's `ground-truth-usuals.json` exists | glm (route-build on `screens/shopping.mjs`; the one-line header note and the deploy command through the same cheap runner) | sonnet (hash, render, re-deploy) + fable (Sienna, six gates) | `curl -sL -o /dev/null -w '%{http_code}' https://skippy-designs.pages.dev/family-pearl-shopping-r10.html` prints 200 and `curl -sL https://skippy-designs.pages.dev/family-pearl-shopping-r10.html \| shasum -a 256` equals `shasum -a 256 projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/shopping.html` | REV + commit hash + hash in §2d; Nick's approval of the published page recorded (the one sanctioned wait — every other step runs meanwhile) |
| Design | 3 | Anchor map additions (#56 onward) including sibling-ORDER anchors, re-signed by Sienna with the counts | STEP 2's published page exists (approval not required) | sonnet | fable (Sienna, creative-director) | `command grep -c '^| [0-9]' projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/gen.mjs-anchors-shopping.md` prints ≥ 81 and `command grep -c 'SIGNED BY' projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/gen.mjs-anchors-shopping.md` prints ≥ 2 | every new drawn element is an anchor or a signed GAP; four ORDER anchors present; hash machine-written |
| Tests | 4 | Suite additions: groups `usuals`/`timeline`, `--order`, `--usuals-states`, `--drive usuals`, `--usuals-phone`, each red-proofed | STEP 3 signed | opus (NICK-ASKED; the typist refuses the suite by label) | sonnet | `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --selftest` prints `red-proof: 1 mismatch (paddingTop) · green-proof: 0 · order-proof: 1 swapped (pl-ustrip) · green-order: 0` and exits 0 | every new mode can go red and green on demand |
| Framing | 5 | The "Bought on" Date column (created by an agent with the vault credential, idempotent) and the one-mutation widening of `functions/api/shopping-complete.js`, red/green on the board | nothing — start now | opus for the column tool (credential at run time); glm via route-build for the function edit | sonnet | `node tools/usuals-bought-on-column.mjs --check` (CREATED BY STEP 5's run) prints `column "Bought on": id <date_…> · present` and `node --check projects/personal/family-app/functions/api/shopping-complete.js` exits 0 | a check-off through the app writes Done AND the date, read back by item id |
| Framing | 6 | The picture route — RETIRED 2026-09-06 | RETIRED 2026-09-06 (PLAN-CHANGES-PEARL-USUALS.md) | — none dispatched; the retired step named glm | — none needed; the retired step named sonnet | RETIRED — the dated entry is in `projects/personal/family-app/PLAN-CHANGES-PEARL-USUALS.md` | RETIRED 2026-09-06 (PLAN-CHANGES-PEARL-USUALS.md) |
| Elements | 7 | `js/pearl-usuals.js` (the derive function, the history query, the render skeleton, the keyword table) + `tools/usuals-derive.test.mjs`; the pull tool re-pointed at the shared derive | STEP 1's file; STEP 5's column id recorded (or `null` recorded) | glm via cheap-task | sonnet | `node tools/usuals-derive.test.mjs` (CREATED BY STEP 7) prints `derive: 12/12 PASS · matches STEP 1's saved list: yes · query equals STEP 1's committed query: yes` | the client derivation equals STEP 1's list on the saved history |
| Framing | 8 | Wire in: link + script tags, `img/usuals-icons.svg` and the three URLs in `sw.js` ASSETS + CACHE, `contract-check.js` lists; painting nothing; the fence pair still 0 | STEP 7's file + STEP 4's modes exist | opus (NICK-ASKED; `index.html`/`sw.js` exceed the typist's size) | sonnet | `node projects/ops/skippy-jobs/_test-family-app-asset-version-parity.mjs` prints PASS and `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --fence-pair off` prints `0 changed elements` at 375 and 1280 | link count = before + 1, script count = before + 1, CACHE = before + 1; the everyday app unchanged |
| Elements | 9 | The rail and the tiles (both widths), the accent sites appended, the skeleton and the empty line | STEP 8 closed; STEP 3's anchors | glm via route-build (`css/pearl-usuals.css`, `js/pearl-usuals.js`) | sonnet | `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --only usuals --inject /abs/css/pearl-usuals.css --inject-js /abs/js/pearl-usuals.js --order` prints `mismatched properties: 0 · unmeasured anchors: 0 · order: 0 differences` at both viewports (CREATED BY STEP 4) | the strip anchors zero at both viewports, DOM order = derived order |
| Elements | 10 | One-tap add: POST, `.pl-on`, the landing motion, the refetch, the U5 string | STEP 9 closed | glm via route-build | sonnet | `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --drive usuals` (CREATED BY STEP 4) prints `usuals: add ✓ (row landed, count +1) · on-list ✓ · fail-string ✓ · removed via check-off ✓` | one injected test tile adds a real item and the app's own check-off removes it, proven gone on a fresh load |
| Details | 11 | The timeline (desktop third column), its order anchor, U14–U17 | STEP 9 closed | glm via route-build | sonnet | `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --only timeline --inject /abs/css/pearl-usuals.css --inject-js /abs/js/pearl-usuals.js --order` prints `mismatched properties: 0 · unmeasured anchors: 0 · order: 0 differences` at 1280×662 and `absent ✓` at 375 (CREATED BY STEP 4) | rows = usuals, date-descending, columns end at 622 |
| Details | 12 | The pictures: the bold-monoline sprite asset on every usual, drawn from the keyword table | STEP 9 closed | glm via cheap-task (the sprite) + route-build (the `use` render) | sonnet | `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --usuals-states` (CREATED BY STEP 4) prints `tiles: N · pictures: 0 · drawn: N · blank: 0`, and `states: 5/5 reached · strings verbatim: K/K` (K read from the `strings` block) | no blank tile in any state |
| Details | 13 | The phone strip one-handed: swipe, 44 px targets, no sideways page scroll, the 1024 band | STEPS 9–12 closed | sonnet (measurement) | glm | `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --usuals-phone` (CREATED BY STEP 4) prints `375: swipe scrollLeft>0 ✓ · targets ≥44 ✓ · page scrollWidth<=clientWidth ✓ · 1024: rail absent · timeline absent ✓` | phone conformance measured |
| Output | 14 | PUBLISH: push branch → main, build, parity, deploy, wall check, served-bytes assertion, fidelity live ×3 at both viewports, side-by-sides, Sienna's verdict, verifier's verdict | STEP 2 APPROVED by Nick; STEPS 4–13 closed | glm (build + deploy commands) | sonnet (re-runs the live check) + fable (Sienna) + verifier | `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --order --out /abs/evidence/usuals-fidelity-live.txt` → `mismatched properties: 0 · unmeasured anchors: 0 · order: 0 differences` ×2 (375×812, 1280×662), three runs each (CREATED BY STEP 4) | all four publish items landed; `/api/finances` anonymous → 401 |
| Proof | 15 | Blind check of every §2 row on the live URL, as Nick and as Chantelle; the live usuals compared to STEP 1's derivation | STEP 14's four items landed | sonnet (se-blind-checker, briefed to REFUTE) | glm (confirms every row cited) | `ls projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/evidence` shows `usuals-blind-check.md` (CREATED BY STEP 15's run) containing `21/21 PASS` | zero failed criteria, or the named failures back to one builder round |
| Proof | 16 | Record: STEPS verified lines, the sp-sec line, the registry append, postmortem, the PROJECT.md sentence for Nick | STEP 15 prints 21/21 | sonnet | glm | `python3 projects/ops/agents/check_plan.py --progress projects/personal/family-app/PLAN-PEARL-USUALS.md (this plan file, CREATED BY STEP 2's first commit)` prints the derived figure and `python3 projects/ops/agents/check_plan.py --artifacts projects/personal/family-app/PLAN-PEARL-USUALS.md` reports nothing MISSING or EMPTY | plan closed at the derived figure; NEXT list holds everything else |

### STEP 1 — Ground truth of the history
**Enter this step when:** nothing. This is the first step.
**RUNNABLE WHEN:** the family vault answers `vault.py get` for the gate entry (the closed plan's STEP 2 used it 2026-09-05) — a refused vault is `STEP 1 BLOCKED — tried: vault get, the linked vault.py in the worktree, the main checkout's vault.py`, never a guess.
**Builder:** opus — `NICK-ASKED: opus — "pull back to opus and sonnet where reasonable for build" (Nick, 2026-09-05)`; the pull signs in through the identity gate with the vault password read at run time (the data floor keeps it off the cheap lane) · **Checker:** sonnet, different session, its own pull
**Files you may touch:** NEW `tools/usuals-pull-history.mjs`, NEW `redesign-mockups/concepts-2026-09-04-round10-screens/ground-truth-usuals.json`, NEW evidence `usuals-step1-pull.txt`. **Never** any live app file, `ground-truth-shopping.json` (the closed plan's), or the board (this step is READ-ONLY: every query it sends is refused by `functions/api/shopping.js` line 27 if it carries `mutation`, and the checker greps the tool for that word: 0 lines).

**Do exactly this:**
1. Write `tools/usuals-pull-history.mjs`: sign in via `projects/shared-tooling/browser.mjs` exactly as `tools/pearl-fidelity-shopping.mjs` does (POST `/__gate`, fields `pw`/`identity`/`next`, the password from `python3 projects/personal/family-vault/vault.py get <the gate entry name> --caller=skippy` at run time — printed nowhere), then from the signed-in page `fetch("/api/shopping", {method:"POST", body:{query}})` with, in order: (a) `boards(ids:[18420185357]){ columns{ id title type settings_str } }` — record every column (id, title, type) and parse the Status column's `settings_str` labels to find DONE_INDEX (the index whose label is "Done"; print it); (b) the Done-filtered `items_page(limit:500, query_params:{rules:[{column_id:"color_mm4vj466", compare_value:[DONE_INDEX], operator:any_of}]}) { cursor items { id name created_at updated_at column_values(ids:["color_mm4vj466","dropdown_mm4vv28h","link_mm57q076"<, the Bought-on id when STATE names one>]) { id text value } } }` then `next_items_page(limit:500, cursor:"…")` until the cursor is null — print the page count and the total; (c) `boards(ids:[18420185357]){ activity_logs(limit:1000, page:P, column_ids:["color_mm4vj466"]) { id event data created_at } }` PAGED — P = 1, 2, 3… until a page returns zero events (cap 20 pages; the page count and whether the cap was hit are printed and saved); parse each `data` JSON for `pulse_id` and the new label; keep the events whose label is "Done"; an item with several Done events contributes EACH distinct Done date as a purchase (a re-opened and re-bought item is two purchases), and its last-bought is the latest; print how many Done items got an activity date (Y), how many fall back to `updated_at` (Z), how many carry Bought-on (X, 0 today).
2. Apply THE HISTORY RULE (§3, (b)–(g)) — this step's copy of the derive function is written into the tool now and REPLACED at STEP 7 by an import of `js/pearl-usuals.js`'s `globalThis.plUsualsDerive` (the tool prints `derive source: inline` today, `derive source: js/pearl-usuals.js` after STEP 7).
3. Write `ground-truth-usuals.json`: `{pulled_at, identity, board:{columns:[…], doneIndex}, done:[{id, name, norm, date, dateSource, category, link:bool}], usuals:[{norm, display, count, last, intervalDays, intervalLabel, store, linkedItemId|null, symbol}], timeline:[{norm, display, last}], symbols:{…the keyword table, copied from the concept's eight product classes plus p-cart…}, counts:{done, usuals, dateRange:[min,max]}}` — money values masked (`$•`), no credential string, no `mutation`.
4. `--check` mode re-derives from the saved file and prints the one-line summary in the DONE-PROOF; `--print` prints the usuals table in plain words for the state file.
5. Save the tool's stdout to `evidence/usuals-step1-pull.txt`; commit the tool, the JSON and the evidence with a pathspec.

**PROOF — all must be true, pasted into STEPS verbatim:**
- `node tools/usuals-pull-history.mjs --check` prints `done items: N · date range: A → B · usuals: M · sources: bought-on X / activity Y / updated_at Z` with N ≥ 1 and N = X + Y + Z (derived, never typed).
- `command grep -c 'mutation' tools/usuals-pull-history.mjs` prints `0`; `command grep -c '\$[0-9]' redesign-mockups/concepts-2026-09-04-round10-screens/ground-truth-usuals.json` (run from the family-app folder) prints `0`.
- The checker's own pull (its own session, its own script or the same tool re-run) prints the same N and the same M names in the same order; a different N is the next finding (a concurrent check-off between the two pulls is the one accepted reason, and the checker names the item).
- What would make this step FAIL, in Nick's words: "that's not what we actually buy" — a usual on the list that the board's Done items do not support twice, or a date not traceable to a board field.
**ARTIFACTS ON DISK**: save `usuals-step1-pull.txt` · save `ground-truth-usuals.json` (both in the round-10 folder tree; the evidence folder is passed to `check_plan.py --artifacts` as its second argument).
**If it fails:** the proxy refuses a query shape (a 400 from Monday) → print the exact GraphQL error and shrink the query (drop `activity_logs`, then `settings_str`), never guess DONE_INDEX; zero Done items on the board → the JSON records `counts.done: 0` and STEP 2 draws the EMPTY state as the default frame (U7/U16) — dependents need this step SUCCEEDED, an empty history is a success with a different drawing.
**Checker's job:** re-run the pull yourself; recount the Done items with a second, differently-shaped query (`items_page` with no rule, filtering Done client-side, all pages); spot-check three usuals' dates against the raw items.
**Handoff:** post `STEP 1 closed <date> — history ground truth at ground-truth-usuals.json (N done, M usuals)` into `STATE-PEARL-USUALS.md`.

### STEP 2 — Lock the target
**Enter this step when:** STEP 1's `ground-truth-usuals.json` exists.
**Builder:** glm through `node projects/ops/route-build.mjs --file projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/screens/shopping.mjs` (one existing file); the `gen.mjs` line-1 REV note, the copy and the deploy command through the same cheap runner (a judgment-free action; the checker re-runs the curl and the three hashes itself) · **Checker:** sonnet, different session · **Design QA:** fable — Sienna (`creative-director`) grades the rendered page on the six creative gates before Nick sees it
**Files you may touch:** `screens/shopping.mjs` (append the usuals rail, the timeline, the sprite, the `extraFrames` and their CSS to `extraCss`; every existing line of `compose()` byte-identical — the checker diffs), `gen.mjs` line 1 only (the REV chain note: `<n> → <n+1> (Usuals lane, <date>): the usuals rail, the last-bought timeline, seven drawn states`), `shopping.html` (regenerated), `projects/personal/skippy-app/design-directions/family-pearl-shopping-r10.html` (the byte-identical copy), NEW `STATE-PEARL-USUALS.md`, NEW `PLAN-CHANGES-PEARL-USUALS.md`, this plan's §2d block and STEPS. **Never** any live app file, the closed plan's files, or the concept pages (read-only reference for the builder: `idea-4-the-usuals.html` and the picked style's block of `usuals-icon-styles.html` are the SOURCE of the markup ported; the four other ideas and five other styles are REJECTED — not opened).

**Do exactly this:**
1. Standing rule (1); probe the checkout: write `evidence/.probe`, read it back, delete it, `git status --porcelain` shows nothing under `evidence/`.
2. In `screens/shopping.mjs`, read `ground-truth-usuals.json` (the To-Do lane's precedent: a screen module reads its own pull) and compose: (a) the rail `<section class="usuals"><div class="utitle"><h2>The usuals</h2><span>…sub-line…</span><span class="from">From the check-off history</span></div><div class="ustrip">…N tiles…</div></section>` placed after the toolbar pill on desktop and after the Add card on phone, one `article.utile[data-usual][data-store]` per usual with `<span class="due">` only when on the list, the picture (`img` placeholder box for linked, `svg use` for unlinked), `.nm`, `.last` ("last bought · N days ago<br>every N days"), `button.add` ("+ Add" / "On the list"); (b) the timeline `<aside class="memory"><h3>Last bought</h3>…mrow…<p class="older">…</p></aside>` as the desktop `.cols2`'s third child with the grid widened to `1fr 1fr 236px` (the closed plan's `.cols2` rule untouched — the widening is a `.desk .cols2.hasmem` rule in `extraCss`); (c) the CSS ported from the concept's `.ustrip/.utile/.memory/.mrow` rules (lines 92–104, 136–145, 147–167) and the picked style's sprite rules, re-tokenised to the design system (`--sans`, `--serif`, `--acc`, `--soft`, `--deep`, `--line`) — every colour a token, no hex except the sprite's tint; (d) `extraFrames`: `Usuals · loading` (skeleton tiles), `Usuals · empty history` (the U7 line, no timeline), `Usuals · no link + failed photo` (two tiles drawn with the sprite), `Usuals · on the list` (a `.on` tile), `Usuals · phone mid-scroll` (the strip translated −200 px), `Usuals · fewer than three` (the U15 footer), each at phone and desktop as the generator draws frames — six extra frames covering §1's seven states; U12 (no timeline on the phone) is a property of the main phone frame, and U19 (the 1024 band) is measured by the suite, never drawn, because §2d's viewports are 375 and 1280; (e) the coverage table appended to the page foot listing every §2 id U1–U18 against the frame that draws it, and U19 marked "measured, not drawn".
3. Every string in the drawing is either the ground-truth file's (names, dates, counts) or one of the drawing's OWN fixed strings, which this step lists in `ground-truth-usuals.json` under `strings` so STEP 4's `--usuals-states` reads them from the file: the title row's three strings, "+ Add", "On the list", "Couldn't add — try again", "bought once", "No usuals yet — check things off and they'll show up here.", "Last bought", "Everything before this sits in the same history — Skippy just stops drawing it.", "That's the whole history so far." — typed here ONCE and copied from the file thereafter (a straight apostrophe typed in this plan is not the curly one the drawing renders; the file wins).
4. Prepend the REV note to `gen.mjs` line 1 (the chain's last number + 1); run the generator for the shopping screen with an absolute path (`node "/Users/nickdeck/Documents/Claude 2.0/.claude/worktrees/pearl-shopping/projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/gen.mjs" shopping`); render with `tools/shot.mjs shopping`; Sienna grades the renders on the six gates (`python3 projects/ops/agents/creative_gates.py --page` for the mechanical half) and her verdict lands in the state file — a FAIL goes back once to the builder before anything is published.
5. `cp` the output to the design-directions copy; `shasum -a 256` both — equal; `node projects/ops/deploy.mjs skippy-designs`; `curl -sL -o /dev/null -w '%{http_code}'` the address → 200; `curl -sL <address> | shasum -a 256` equals the local hash — the three hashes written into `STATE-PEARL-USUALS.md` and §2d IN THE SAME SHELL (never typed from a report).
6. Commit with a pathspec (`screens/shopping.mjs`, `gen.mjs`, `shopping.html`, the copy, the two new state files, this plan); record the hash; `git merge-base --is-ancestor <hash> HEAD`; push the branch with `--force-with-lease`; write `REV <n+1> · commit <hash>` into §2d.
7. Create `STATE-PEARL-USUALS.md` (current-state only: step, next step, handoffs, blocked lines, the three hashes, the column id when known) and `PLAN-CHANGES-PEARL-USUALS.md` (line 1 reserved for the cold reader's verdict, already written by the plan author if it landed before this step).
8. Grep this plan for the registry's template-placeholder vocabulary: `command grep -nE '<BUILDER:|<NICK:|<paste|<path>|<evidence>|\(measured\)' PLAN-PEARL-USUALS.md` (run from the family-app folder) prints nothing. Run-time tokens the plan deliberately carries — `<today>`, `<run id>`, `<n+1>`, `<abs …>`, `<the evidence folder>` — are values a builder substitutes at run time, not placeholders, and every one is defined where it is used.
9. Hand Nick ONE message: the address, plus the confirmation sheet rendered by `python3 projects/ops/agents/render_sheet.py` on this plan (seven rows in plain English; rows 1, 2, 3 and 7 are CONFIRMED in his own 2026-09-06 words and are never re-asked; the three still-DEFAULTED rows — 4, 5 and 6 — are his to override in one word each) — and ask nothing else; his yes lands as `Nick, <date>, "<words>"` in §2d. This is the ONE sanctioned wait; every step whose gate does not name "STEP 2 APPROVED" runs meanwhile.

**PROOF — all must be true, pasted into STEPS verbatim:**
- `command grep -c '^// REV ' projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/gen.mjs` prints `1`; the curl prints `200`; the three sha256 lines are identical.
- `git diff <parent>..<hash> -- projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/screens/shopping.mjs` shows only ADDED lines inside `compose()` and `extraCss` (zero `-` lines other than the closing of the `desk`/`phone` template literals and the return).
- The page's coverage table lists U1–U18 each against the frame that draws it and U19 as "measured, not drawn"; Sienna's six-gate verdict line is in the state file.
- What would make this step FAIL, in Nick's words: "that's not the one I approved" — a published page whose hash differs from the generator output, or a revision he has not seen; "the icons are weak" — a sprite not in the picked style.
**ARTIFACTS ON DISK**: save `usuals-step2-hashes.txt` · save `usuals-step2-sienna.md`
**If it fails:** deploy refused → the published address keeps the closed plan's REV; post one line to the state file; STEPS 3–13 run regardless against the LOCAL output (the suite's `--target-file`); STEP 14 cannot open until this lands (it needs the approved revision, SUCCEEDED).
**Checker's job:** re-run the curl and the three hashes yourself; open the published page and the local render and confirm they are the same drawing; count the frames in the coverage table.
**Handoff:** post `STEP 2 closed <date> — REV <n+1> published; the To-Do and Extras lanes' generator header moved by one line` into `PLAN-CHANGES-PEARL-USUALS.md` and, as a courtesy line, into `PLAN-CHANGES-PEARL-SHOPPING.md` (the generator is shared).

### STEP 3 — Anchor map additions, signed
**Enter this step when:** STEP 2's published page exists (approval not required yet) and STEP 1's file exists.
**Builder:** sonnet (maps never go through the cheap lane — DESIGN-FIDELITY-STANDARD, the "cheap vendor on a verdict or map" trap) · **Checker/signer:** fable — Sienna (`creative-director`), different session
**Files you may touch:** `gen.mjs-anchors-shopping.md` (APPEND rows #56 onward and a second `SIGNED BY` line; rows #1–#55 byte-identical — the checker diffs). **Never** the generator or the suite.

**Do exactly this:**
1. List every distinct element the extended drawing draws that the 55-row map does not already cover: the rail section, the title row (h2, sub-line, "from" label), a tile (linked · unlinked · on-the-list · skeleton), the tile picture (`img`), the tile drawn asset (`svg`), the name, the last line, the add button (default · on), the dot, the empty line, the timeline aside, its heading, a row (recent · older), the dot pseudo-element, the date, the name, the footer (default · fewer-than-three). Write the count.
2. One row per element: `design selector` (inside `.desk`/`.frame` of the published page, or the named extra frame) → `live selector` (inside `body.skin-pearl #view-shopping .pl-usuals` / `.pl-memory` as §3's contract names them: `.pl-utitle`, `.pl-ustrip`, `.pl-utile`, `.pl-upic`, `.pl-usk-ic`, `.pl-unm`, `.pl-ulast`, `.pl-uadd`, `.pl-udue`, `.pl-usk`, `.pl-unone`, `.pl-memory`, `.pl-mh`, `.pl-mrow`, `.pl-md`, `.pl-mp`, `.pl-mold`) → properties compared (PROPS+BOX by default; `objectFit`, `aspectRatio` for the picture; `display` for the phone-absent timeline) → `GAP` with reason where no live hook exists.
3. Four sibling-ORDER anchor rows, kind `ORDER`: `.pl-ustrip` children by `data-usual` (design order = ground-truth order); `.pl-memory` children by date; the desktop content box's children (`.pl-shtb` → `.pl-usuals` → `.pl-cols2`); the phone stack's children (`.shop-add-card` → `.pl-usuals` → the first `.shop-panel`). Each names its container selector on both sides and the property compared: DOM order AND painted top (`getBoundingClientRect().top` ascending).
4. `VIEWPORTS: 375×812 light · 1280×662 light` restated; `RETIRED:` unchanged.
5. Sienna signs a second line: `SIGNED BY sienna <date> — usuals elements drawn: N · anchors added: M · order anchors: 4 · gaps: K` with M ≥ N − K and K ≤ 2.

**PROOF:** the map has ≥ 81 numbered rows (55 + ≥ 22 element anchors for the ~24 elements item 1 enumerates + 4 ORDER anchors); the second signature line present with M ≥ N − K and K ≤ 2; a `node -e` over `usuals-step3-selector-counts.json` prints `rows: M · count≠1: 0` (every new design selector matched exactly one node on the published page); the machine-written hash of the map in the state file. FAIL: fewer anchors than drawn elements, an ORDER anchor missing for a container the drawing orders, or a GAP without a reason.
**ARTIFACTS ON DISK**: save `usuals-step3-selector-counts.json` (every design selector's `querySelectorAll` count on the published page = 1, measured through the rig, never asserted)
**If it fails:** three GAPs → the drawing or the hooks are wrong; one line to the overseer naming which; STEP 4 needs this ANSWERED (signed).
**Checker's job (Sienna):** count the drawn elements yourself from the published page; refuse any anchor whose live selector would match more than one node; confirm each ORDER anchor's container is one the drawing actually orders.

### STEP 4 — Suite additions, red-proofed
**Enter this step when:** STEP 3 is signed.
**Builder:** opus — `NICK-ASKED: opus — "pull back to opus and sonnet where reasonable for build" (Nick, 2026-09-05)`; the cheap typist refuses `tools/pearl-fidelity-shopping.mjs` by label (recorded three times on 2026-09-06), so the hunks are applied on Anthropic with the identical proof; the refusal is re-tried ONCE through `route-build.mjs` first and its refusal text saved · **Checker:** sonnet
**Files you may touch:** `tools/pearl-fidelity-shopping.mjs` (append-only: new ROWS entries #56+, two new group names, five new modes; every existing row and mode byte-identical — the checker diffs). **Never** the reference script or the map.

**Do exactly this:**
1. Add the new ROWS from the signed map (STEP 3), groups `usuals` and `timeline`; ORDER rows carry `kind:"order"`, a container selector per side, and `by` (`data-usual` | `date` | `child index`).
2. `--order`: for every `kind:"order"` row, read the container's children on the design and on the live page, print `order <container>: design [a,b,c…] · live [a,b,c…] · painted tops ascending: yes/no`, count differences, and add `order: N differences` to the summary line; exit non-zero on N > 0.
3. `--usuals-states`: force U6 (throttle the history query with `Network.emulateNetworkConditions` and capture before it returns), U7 (substitute the proxy's response with an empty `items` array via `Fetch.fulfillRequest`), U8 (`Fetch.failRequest` on the history query), U10 (fulfil `/api/link-image` with 204 for one tile; fulfil with a broken image body for another), U15 (substitute a two-item history); for each, capture, assert the state's marker (`.pl-usk` count 8 · `.pl-unone` text · `[hidden]` on `.pl-usuals` · the swapped `svg` · the footer text) and compare every fixed string byte-for-byte to `ground-truth-usuals.json`'s `strings` block (read at run time); print `states: 5/5 reached · strings verbatim: K/K` (K = the number of entries in the `strings` block, read at run time, never typed — nine on 2026-09-06; every entry is asserted somewhere across the five states and the default render) and `tiles: N · pictures: P · drawn: D · blank: 0` (blank = a tile with neither a loaded `img` (`naturalWidth > 0`) nor a `use` whose symbol exists in the sprite).
4. `--drive usuals`: inject ONE test tile (`data-usual="pearl-usuals-check-<today>-<run id>" data-store="walmart"`, where `<run id>` is four random hex characters minted per run and printed — so no two runs, on any day, share a normalised name, and a test item can never reach §3's ≥ 2 rule or the timeline) into `.pl-ustrip` through `--inject-js`, click its add button, assert the POST to `/api/shopping-add` returned 2xx, the tile carries `.pl-on`, a `.shop-prio` row named `pearl-usuals-check-<today>-<run id>` appears in `#shop-walmart` with `.pl-landed` present within one animation frame, the header count rose by one; then force a 500 on the same POST for a second injected tile and assert the U5 string; then check the test row off THROUGH THE APP'S OWN CONTROL (`.chk` → the app's confirm → accept) and assert on a fresh load that the row is gone; print the four ✓ lines. Order via Skippy is never clicked.
5. `--usuals-phone`: at 375×812 dispatch a touch swipe (`Input.dispatchTouchEvent` start/move/end, 200 px leftwards) on `.pl-ustrip`; assert `scrollLeft > 0`; assert every `.pl-uadd` and `.pl-utile` `getBoundingClientRect().height >= 44` for the button; assert `document.documentElement.scrollWidth <= clientWidth`; at 1024×768 assert `.pl-rail` (the desktop navigation MENU, the Home lane's node — not this plan's usuals rail, which is `.pl-usuals`) absent, `.pl-memory` absent and `.pl-usuals` present; print the summary line.
6. Extend `--selftest`: the existing padding red-proof stays; add the ORDER red-proof (swap two `.pl-ustrip` children in an injected copy of the design and assert `order: 1 differences` naming `pl-ustrip`), then the green run at 0; the sabotage stylesheet now also breaks the new anchors' compared properties and the run names every one of them red.
7. `--bogus-flag` still exits 2 with `unknown flag`.

**PROOF:** `--selftest` prints `red-proof: 1 mismatch (paddingTop) · green-proof: 0 · order-proof: 1 swapped (pl-ustrip) · green-order: 0`, exit 0; the sabotage names every compared property of rows #56+; `--bogus-flag` exits 2. FAIL: a run that prints 0 with an anchor it never measured, or an ORDER mode that passes a swapped pair.
**ARTIFACTS ON DISK**: save `usuals-step4-selftest.txt` · save `usuals-step4-sabotage.txt`
**If it fails:** the Chrome lock held → wait on the lock (it force-breaks at 45 min), never a second Chrome; dependents need this SUCCEEDED.
**Checker's job:** run `--selftest` yourself; break one new anchor's live selector on purpose (a typo) and confirm the run exits 2 with that anchor named UNMEASURED; swap two timeline rows in an injected design copy and confirm `--order` goes red naming `pl-memory`.

### STEP 5 — The "Bought on" column and the check-off that sets it
**Enter this step when:** nothing. Runs in parallel with STEPS 1–4.
**RUNNABLE WHEN:** the vault answers `vault.py get monday-api-token --caller=skippy` (entry listed by name 2026-09-06) — the value is read into the tool's process at run time and never printed, written or passed on a command line.
**Builder:** opus for `tools/usuals-bought-on-column.mjs` — `NICK-ASKED: opus — "pull back to opus and sonnet where reasonable for build" (Nick, 2026-09-05)`; it holds a credential at run time, the data floor's own limb · glm through `node projects/ops/route-build.mjs --file projects/personal/family-app/functions/api/shopping-complete.js` for the function edit (the brief says "credential" and "the board's date column", never the tripping word; if the lane refuses twice, the same hunk is applied on opus with the identical proof and the refusal recorded) · **Checker:** sonnet
**Files you may touch:** NEW `tools/usuals-bought-on-column.mjs`, `functions/api/shopping-complete.js` (the mutation block lines 26–34 and ONE new constant; every other line byte-identical), `STATE-PEARL-USUALS.md` (the column id). **Never** `functions/api/shopping.js`, `functions/api/shopping-add.js`, `js/app.js`.

**Do exactly this:**
1. `tools/usuals-bought-on-column.mjs`: read the credential from the vault at run time; `--check` queries `boards(ids:[18420185357]){ columns { id title type } }` and prints `column "Bought on": id <id> · present` or `absent`; `--create` runs `create_column(board_id:18420185357, title:"Bought on", column_type:date)` ONLY when `--check` printed `absent`, then re-runs `--check` and prints the new id. Board columns are a config change (no approval class); the tool is idempotent and refuses to create a second column with that title.
2. Run `--check`, then `--create` if absent, then `--check` again; write the id into `STATE-PEARL-USUALS.md` under `BOUGHT_ON_COLUMN_ID`.
3. `functions/api/shopping-complete.js`: add `const BOUGHT_ON_COLUMN_ID = "<the id>";` beside `STATUS_COLUMN_ID` (line 12) and change the mutation to `change_multiple_column_values(board_id:$boardId, item_id:$itemId, column_values:$values)` with `values = JSON.stringify({ [STATUS_COLUMN_ID]: { label: DONE_LABEL }, [BOUGHT_ON_COLUMN_ID]: { date: today } })` where `today` = `new Intl.DateTimeFormat("en-CA", { timeZone: "America/Cancun" }).format(new Date())` (YYYY-MM-DD); the response passthrough, the id validation and every header unchanged.
4. Red first, as a unit test (`tools/usuals-complete.test.mjs`, NEW, this step): import the function's handler with a fake `env` and a fake `fetch` that captures the GraphQL body; BEFORE the edit the captured mutation is `change_column_value` carrying only the Status label (the red, saved); AFTER, it is `change_multiple_column_values` whose `column_values` JSON carries BOTH the Status label and `{date: <the fake clock's Cancún date>}` under the recorded column id, and every other assertion (id validation, headers, passthrough) unchanged (the green). The LIVE read-back — add `pearl-usuals-check-<today>-<run id>-<run id>` on Walmart/Local with the app's own Add form, check it off through the app's own `.chk` → confirm, read the item back through the proxy by id: Status Done AND `Bought on` = today — runs inside STEP 14.7 (U22) and is pasted into this step's STEPS entry then. The test item stays Done (a single Done occurrence with a unique name is never a usual).
5. `node --check` on the function; commit with a pathspec.

**PROOF:** `node tools/usuals-bought-on-column.mjs --check` prints `column "Bought on": id date_… · present`; `git diff` of `shopping-complete.js` touches only the constant line and the mutation block (the checker counts the changed hunks: 2); `node tools/usuals-complete.test.mjs` (CREATED BY STEP 5's run) prints `complete: red saved · 4/4 PASS`; `node --check` exits 0; the live read-back (U22) is pasted here when STEP 14.7 runs it. FAIL, in Nick's words: "it checked off but forgot the date" — a Done write with no date after the edit.
**ARTIFACTS ON DISK**: save `usuals-step5-column.txt` · save `usuals-step5-redgreen.txt`
**If it fails:** `create_column` refused by Monday (a permission scope on the token) → record the exact error, leave the constant `null`, and the derivation falls back to the activity log / `updated_at` (§3 (d)) — dependents need this step ANSWERED (id or null), not necessarily SUCCEEDED; the id is retried at STEP 16 as a NEXT-list item if still null.
**Checker's job:** run `--check` yourself; diff the function against its snapshot; read the test item back yourself.

### STEP 6 — The picture route
**RETIRED 2026-09-06 — Nick's icon answer was "bold monoline" alone (§1a rows 2–3); no photo route is built in this plan; the design of this step is preserved below for the NEXT-list round and no builder is dispatched to it.**
**Builder:** glm through `node projects/ops/cheap-task.mjs --do "create functions/api/link-image.js and tools/usuals-link-image.test.mjs …" --dir projects/personal/family-app --prove "node tools/usuals-link-image.test.mjs"` (a brand-new file: cheap-task, never route-build) · **Checker:** sonnet (the unit proof now; the live curls run inside STEP 14.7 and are pasted into this step's STEPS entry)
**Files you may touch:** NEW `functions/api/link-image.js`, NEW `tools/usuals-link-image.test.mjs` (a node test that imports the function's handler with a fake `env.FAMILY_FINANCES` (a Map), a fake `fetch` serving fixture pages, and a fake Monday reply). **Never** `functions/api/ollie-img.js` (the pattern copied, not edited), `functions/_middleware.js`.

**Do exactly this:**
1. `onRequestGet({request, env})`: `item` must match `/^\d{6,20}$/` else 400; `env.FAMILY_FINANCES` absent → 500 `no binding` (ollie-img's shape); read `shop-img:<id>`: a `{b64, mime}` record → bytes with `Content-Type: mime`, `Cache-Control: public, max-age=2592000`, `x-usuals-cache: hit`; a `{none:true, at}` record younger than 7 days → 204 `x-usuals-cache: none`.
2. On a miss: query Monday server-side with `env.MONDAY_API_TOKEN` (the same header shape as `shopping.js` lines 32–40) for `items(ids:[<id>]){ column_values(ids:["link_mm57q076"]){ text value } }`; take the URL from the column's `value.url` first, else a bare-URL `text` (the app's own rule, `js/app.js` lines 2072–2079); accept only `http:`/`https:`, a hostname containing a dot, not an IP literal; anything else → write the negative marker, 204.
3. Fetch the page (6 s `AbortController` timeout, `User-Agent` of a current desktop browser, `Accept: text/html`); on non-2xx or non-HTML → negative marker, 204; parse the first of `og:image` · `twitter:image` · `link[rel=image_src]` · the first `<img>` whose `src` contains `images/I/` or `product` (a regex over the first 512 KB of HTML; no DOM library); resolve relative to the page URL; the same URL rules as step 2.
4. Fetch the picture (6 s timeout; ALWAYS pass `cf:{image:{width:400, fit:"scale-down"}}` — item 5 says why and how it is measured); require `Content-Type: image/*` and a body ≤ 250 KB, else negative marker, 204; store `{b64, mime, at}` under `shop-img:<id>`; respond 200 with the bytes and `x-usuals-cache: miss`.
5. Resizing is MEASURED, never assumed, and no test-only branch ships: the picture fetch always passes `cf:{image:{width:400, fit:"scale-down"}}` (a zone without the Image Resizing feature ignores the option — Cloudflare documents it as a no-op there — so the 250 KB byte cap is the only guarantee); inside STEP 14.7 the checker downloads the first real picture the route served and reads its width with `sips -g pixelWidth`, and the state file records `resize: measured yes` (≤ 400 px) or `resize: measured no` (wider) with the item id. Either answer is a pass; a blank tile is the only fail.
6. File the security-shaped line NOW, in this step, not later: append ONE line to `projects/ops/sp-sec/PLAN.md` — `<date> · functions/api/link-image.js is the family app's first server-side outbound fetcher (URL from the board's link column only, never the request; http(s), dotted host, no IP literal; 6 s timeouts; 250 KB cap) — for the end-phase security pass` — and return to this step. Nick's §S ruling (2026-09-04: the security pass happens at the end, as its own phase) is why the route ships before that pass; the URL rule above is the design choice that keeps it narrow, not an audit.
7. The unit test (8 cases): bad id → 400; no binding → 500; cache hit → 200 + `hit`; negative marker fresh → 204; negative marker stale → refetch; no link → 204 + marker written; page with `og:image` → 200 + record written with the right mime; picture over 250 KB → 204 + marker. Run RED first by asserting a wrong status on one case and watching it fail, then green.

**PROOF:** `node tools/usuals-link-image.test.mjs` prints `8/8 PASS`; `node --check functions/api/link-image.js` exits 0; `command grep -c 'FAMILY_FINANCES' functions/api/link-image.js` prints ≥ 2 (the binding guard and the read); `command grep -c 'searchParams.get("u")\|body.url' functions/api/link-image.js` prints `0` (the URL never comes from the request). FAIL: a picture served for a URL the request supplied, or a blank tile because a 204 was not returned on failure.
**ARTIFACTS ON DISK**: save `usuals-step6-unit.txt`
**If it fails:** the cheap lane's proof passes on a deletion (the trap of 2026-08-24) → the checker's `node --check` + the 8-case test are the proof, not "it parses"; dependents (STEP 12) need this SUCCEEDED for the unit half; the live half lands with STEP 14. If Nick answers NO on sheet row 3 (no photos): this file stays committed and unwired to any tile — STEP 12 renders the sprite only and never requests the route; one line in the state file, no rebuild.
**Checker's job:** run the test yourself; read the handler and confirm the four exits (400 · 500 · 204 · 200) and the byte cap are literal; inside STEP 14.7, `curl -s -o /dev/null -w '%{http_code} %{content_type}\n'` the route for one linked usual (200 image/…, then `hit` on the second call), one unlinked (204), and `?item=abc` (400), signed in with the planted cookie.

### STEP 7 — The client derivation and the history query
**Enter this step when:** STEP 1's `ground-truth-usuals.json` exists and STEP 5 has recorded the column id (or `null`).
**Builder:** glm through `node projects/ops/cheap-task.mjs --do "create js/pearl-usuals.js and tools/usuals-derive.test.mjs …" --dir projects/personal/family-app --prove "node tools/usuals-derive.test.mjs"` · **Checker:** sonnet
**Files you may touch:** NEW `js/pearl-usuals.js` (a header comment, the guard, `globalThis.plUsualsDerive`, the history query string, the keyword table, the observer skeleton — NO rendering yet), NEW `tools/usuals-derive.test.mjs`, `tools/usuals-pull-history.mjs` (STEP 1's — its inline derive REPLACED by an import of `js/pearl-usuals.js`'s exported function; the file loads the script with `globalThis` as the sandbox). **Never** `js/pearl-shopping.js`, `js/app.js`.

**Do exactly this:**
1. `js/pearl-usuals.js` opens with the guard (`body.skin-pearl` and `#view-shopping` present, else return) and exposes `globalThis.plUsualsDerive(doneItems, openNames, opts)` implementing §3 (b)–(h) exactly, pure and side-effect-free, returning `{usuals:[…], timeline:[…], empty:bool}`; `opts.now` is injectable so the tests pin the clock.
2. The history query string `PL_USUALS_QUERY(doneIndex, cursor, boughtOnId)` builds exactly STEP 1's query (the checker diffs the two strings after templating); `plUsualsFetchHistory()` POSTs `/api/shopping` first for the columns + `settings_str`, then the Done pages by cursor, then the activity log; returns the flat Done list with `{id, name, date, dateSource, category, link}`; any non-2xx or thrown error resolves to `{error: status}` (U8).
3. The keyword table `PL_USUALS_SYMBOLS` copied from `ground-truth-usuals.json`'s `symbols` block (the checker diffs).
4. A `MutationObserver` skeleton over `#view-shopping` with the closed plan's re-entrancy guard and microtask debounce that, on each app render after `.pl-shtb` exists, calls `render()` — which in THIS step only computes the derivation and logs `[usuals] N usuals` (no DOM writes; STEP 9 adds them).
5. `tools/usuals-derive.test.mjs`: loads `js/pearl-usuals.js` into a `vm` context with a fake `document.body.classList` lacking `skin-pearl` (so the guard returns) and reads `globalThis.plUsualsDerive`; 12 cases: normalisation (parentheses, dashes, case, spaces), the ≥ 2 rule, count-then-recency ordering, the median interval on 2/3/5 dates, "bought once", days vs weeks wording, the store vote, the no-store chip, the on-list match, the N cap, the empty result, and — the one that carries the weight — the client function run on the saved `done` array reproducing STEP 1's SAVED `usuals` array exactly (that array was produced by STEP 1's INLINE derive, read from the committed file at STEP 1's hash, so the two implementations are independent and a disagreement goes red); the last line also diffs the client's query string against the pull tool's literal as committed at STEP 1's hash (`git show <STEP 1's hash>:<the pull tool's repo path>`) and prints `query equals STEP 1's committed query: yes`. Only after both pass is the pull tool re-pointed at the shared function.

**PROOF:** `node tools/usuals-derive.test.mjs` prints `derive: 12/12 PASS · matches STEP 1's saved list: yes · query equals STEP 1's committed query: yes`; `node tools/usuals-pull-history.mjs --check` still prints STEP 1's summary line with `derive source: js/pearl-usuals.js`; `node --check js/pearl-usuals.js` exits 0; `command grep -c 'fetch("/api/shopping-add"' js/pearl-usuals.js` prints `0` (the add arrives at STEP 10). FAIL: a derivation that differs from STEP 1's saved list, or a query string that differs from STEP 1's.
**ARTIFACTS ON DISK**: save `usuals-step7-derive.txt`
**If it fails:** the cheap lane's proof passes while the test file was gutted → the checker re-runs and counts the 12 case names; dependents (STEPS 8–12) need this SUCCEEDED.
**Checker's job:** run the test yourself; delete one case and confirm the count drops; diff the query string against STEP 1's tool.

### STEP 8 — Wire in, painting nothing
**Enter this step when:** STEP 7's `js/pearl-usuals.js` exists, STEP 4's modes exist, and `css/pearl-usuals.css` and `img/usuals-icons.svg` exist as header-only / empty-symbol stubs (created in this step's first action).
**Builder:** opus — `NICK-ASKED: opus — "pull back to opus and sonnet where reasonable for build" (Nick, 2026-09-05)`; `index.html` (632 KB) and `sw.js` (178 KB) exceed the typist's size; `contract-check.js` may go through route-build · **Checker:** sonnet
**Files you may touch:** `index.html` (one link tag + one script tag, last in their groups), `sw.js` (ASSETS + CACHE = before + 1), `contract-check.js` (only entries its coverage gates name), NEW `css/pearl-usuals.css` (header comment only), NEW `img/usuals-icons.svg` (an empty `<svg><defs></defs></svg>` with a header comment). **Never** anything else. This is the ONLY step that edits the three shared files; re-read each immediately before writing; scoped commit; not in the same hour as any other lane's step that touches them (the state file records the hour).

**Do exactly this:**
0. Create the two stubs FIRST: `css/pearl-usuals.css` (one header comment naming this plan, zero rules) and `img/usuals-icons.svg` (`<svg xmlns="http://www.w3.org/2000/svg"><defs></defs></svg>` with one header comment); `js/pearl-usuals.js` already exists from STEP 7. Nothing below may reference a file that is not on disk.
1. Capture the baseline first: `--fence-pair off` at 375 and 1280 → `evidence/usuals-fence-baseline.json`.
2. Add `<link rel="stylesheet" href="css/pearl-usuals.css?v=1">` after `css/pearl-shopping.css`'s link and before `css/pearl-nav.css`; add `<script defer src="js/pearl-usuals.js?v=1">` after `js/pearl-shopping.js`'s script tag.
3. `sw.js`: ASSETS gains the three URLs `css/pearl-usuals.css?v=1`, `js/pearl-usuals.js?v=1`, `img/usuals-icons.svg`, each written in the list's own dot-slash form; `const CACHE` = previous + 1 (derive; never type a number twice).
4. Run `node projects/personal/family-app/contract-check.js`; add EXACTLY the names its coverage gates print; re-run green; the checker removes them and confirms red for exactly those names.
5. `node projects/ops/skippy-jobs/_test-family-app-asset-version-parity.mjs` → PASS.

**PROOF:** stylesheet-link count = before + 1 (derived), script count = before + 1; the three ASSETS entries each resolve to a file in the staged `dist/` after `build-dist.js` (an `ls` of the three paths, all present, sizes printed); parity PASS; contract-check green with the number quoted; `--fence-pair off` prints `0 changed elements` at both viewports AND `--fence-pair on` (this lane's two files active vs inactive within one frozen session) prints `0 changed elements` (nothing composed yet). FAIL: any element moves in the everyday app, or a paint before composition.
**ARTIFACTS ON DISK**: save `usuals-step8-wire.txt` · save `usuals-fence-baseline.json`
**If it fails:** parity red → read what it names (usually a `?v=` mismatch) before touching anything; dependents need this SUCCEEDED.
**Checker's job:** re-run parity, contract-check and both fence runs yourself.

### STEP 9 — The rail and the tiles
**Enter this step when:** STEP 8 is closed and STEP 3's anchors are signed.
**Builder:** glm through `node projects/ops/route-build.mjs --file …/css/pearl-usuals.css` and a second run on `…/js/pearl-usuals.js` (one file per run) · **Checker:** sonnet
**Files you may touch:** `css/pearl-usuals.css`, `js/pearl-usuals.js`, `tools/pearl-accent-sites-shopping.json` (APPEND the four sites in §3). **Never** `js/pearl-shopping.js`, `css/pearl-shopping.css`, `js/app.js`.

**Do exactly this:**
1. `render()` now builds the section once per app render: `section.pl-usuals` → `.pl-utitle` (h2 "The usuals", the sub-line, `.pl-ufrom`) → `.pl-ustrip` → N `article.pl-utile[data-usual][data-store][data-item]`, each with `.pl-upic` (`svg.pl-usk-ic > use[href="img/usuals-icons.svg#<symbol>"]` for EVERY usual — no `img` and no picture request; §1a row 3), `.pl-unm`, `.pl-ulast` (two lines), `button.pl-uadd` ("+ Add"), and `.pl-udue` + `.pl-on` when on the list; before the history resolves, eight `.pl-utile.pl-usk` skeletons; on `{error}` the section gets `hidden`; on `empty` the section holds only `.pl-unone` with the drawing's string (all strings read from a `PL_USUALS_STRINGS` object whose values the checker diffs against `ground-truth-usuals.json` `strings`).
2. Placement: at ≥1100 (`matchMedia`) as the next sibling of `.pl-shtb`; below, as the next sibling of `.shop-add-card`; re-placed on every render (the closed plan's pop.js-re-render lesson: restate the order every time).
3. CSS ported from the drawing's tokens: the rail grid `repeat(8, minmax(0,1fr))` gap 11 px at ≥1100; the phone strip `display:flex; overflow-x:auto; scrollbar-width:none; gap:10px` with 132 px tiles and 44 px-tall add buttons; every colour a token from the closed plan's scope; the skeleton tiles' box equal to a real tile's.
4. Append the four accent sites to `tools/pearl-accent-sites-shopping.json`.

**PROOF:** `--only usuals --inject … --inject-js … --order` prints `mismatched properties: 0 · unmeasured anchors: 0 · order: 0 differences` at both viewports; `--fence-pair off` still 0; the accent-site count on the live page equals the site list's count + the closed plan's. FAIL: a second accent hue, the accent on a label, a tile out of derived order, or a blank tile.
**ARTIFACTS ON DISK**: save `usuals-step9-check.txt`
**If it fails:** an anchor that cannot reach zero because the drawing and the hook disagree → a GAP proposal to Sienna (STEP 3 re-sign), never a silent tolerance; dependents need this SUCCEEDED.
**Checker's job:** re-run the injected check; count accent-coloured elements against the site list; read the DOM order yourself.

### STEP 10 — One-tap add
**Enter this step when:** STEP 9 is closed.
**Builder:** glm through `route-build.mjs` on `js/pearl-usuals.js` (and `css/pearl-usuals.css` for the landing motion) · **Checker:** sonnet
**Files you may touch:** `js/pearl-usuals.js`, `css/pearl-usuals.css`. **Never** `functions/api/shopping-add.js`, `js/app.js`.

**Do exactly this:**
1. Click on `.pl-uadd` of a tile not `.pl-on` and with a store: POST `/api/shopping-add` `{name: <display name>, store: <data-store>, qty: 1}` (`credentials: "same-origin"`); while pending the button reads "Adding…" and is `aria-busy`; on 2xx → the tile gains `.pl-on` + `.pl-udue`, the button reads "On the list" and `aria-disabled="true"`, then `loadShopping()` is called by bare name (guarded by `typeof`), and after the app's re-render the new `.shop-prio` row whose `.pt` normalises to the usual gets `.pl-landed` for one animation (the drawing's `landed` keyframes, 500 ms) and the class is removed on `animationend`; on non-2xx or a throw → the button reads "Couldn't add — try again" for 4 s then "+ Add" again; a tile with no store never posts.
2. No confirm dialog (§1a row 6); a double-tap within the pending window is ignored.

**PROOF:** `--drive usuals` prints `usuals: add ✓ (row landed, count +1) · on-list ✓ · fail-string ✓ · removed via check-off ✓`; the board read-back for `pearl-usuals-check-<today>-<run id>` shows the item created on Walmart/Local with qty 1, then Done with Bought on = today (STEP 5 live) after the app's own check-off; a fresh load shows the row gone. FAIL: an add with no row landing, a tile that stays "+ Add" after a 2xx, or a POST from a store-less tile.
**ARTIFACTS ON DISK**: save `usuals-step10-drive.txt`
**If it fails:** `loadShopping` not reachable by bare name (measured `typeof` prints `undefined`) → record it, and the tile falls back to the U5 string with a console line — dependents need this ANSWERED; the app's refetch is the closed plan's territory and gets a handoff line, never an edit to `js/app.js`.
**Checker's job:** re-run `--drive usuals` yourself (it mints its own run id); read the board back by id.

### STEP 11 — The last-bought timeline
**Enter this step when:** STEP 9 is closed (parallel with STEP 10).
**Builder:** glm through `route-build.mjs` on `js/pearl-usuals.js`, then on `css/pearl-usuals.css` · **Checker:** sonnet
**Files you may touch:** `js/pearl-usuals.js`, `css/pearl-usuals.css`. **Never** `css/pearl-shopping.css` (its `.pl-cols2` rule stays; this lane widens the grid only through its own `:has(> .pl-memory)` rule).

**Do exactly this:**
1. At ≥1100 only (`matchMedia`; §1a row 5), `render()` appends `aside.pl-memory` as the LAST child of `.pl-cols2`: `h3.pl-mh` "Last bought", one `.pl-mrow[data-usual]` per usual in last-bought-descending order (`.pl-md` the date as "Sep 1" in America/Cancun, `.pl-mp > b` the display name), the two most recent rows `.pl-recent` (the filled accent dot via `::before`), and `p.pl-mold` with the footer — the "fewer than three" footer when the whole Done history has fewer than three items (the `strings` block names both). Below 1100 the aside is never created; on `empty` it is never created.
2. CSS: the grid rule `body.skin-pearl #view-shopping .pl-cols2:has(> .pl-memory){grid-template-columns:1fr 1fr 236px}`; the aside `border-left` hairline, `padding-left 18px`, `overflow-y:auto; min-height:0` so it scrolls inside the 662 frame and ends at the menu's bottom like the columns (the shell's flex column already sizes the grid); the dots, dates and names from the drawing's tokens.

**PROOF:** `--only timeline --inject … --inject-js … --order` prints `mismatched properties: 0 · unmeasured anchors: 0 · order: 0 differences` at 1280×662 and `absent ✓` at 375; the closed plan's `redesign-mockups/concepts-2026-09-04-round10-screens/evidence/shopping-step9-frameproof.mjs` re-run prints `frame: 662 · rail: 40→622 · col1: →622 · col2: →622` unchanged plus this lane's `--only layout` reads the third column's bottom ≤ 622; `--usuals-states` reaches U15 with the footer verbatim. FAIL: a row out of date order, a phone timeline, or a column overflowing the frame.
**ARTIFACTS ON DISK**: save `usuals-step11-check.txt`
**If it fails:** the third column pushes a list column past 622 → the grid's `minmax(0,1fr)` and the aside's `min-height:0` are the fix, never a min-height; dependents (STEP 13, 14) need this SUCCEEDED.
**Checker's job:** re-run; resize to 1280×800 and confirm the three columns still meet the rail's bottom; read the dates against STEP 1's timeline array.

### STEP 12 — The pictures
**Enter this step when:** STEP 9 is closed. This step CLOSES on the injected run against the local files; its live line is re-run inside STEP 14.7 and pasted into this step's STEPS entry then.
**Builder:** glm through `cheap-task.mjs` for `img/usuals-icons.svg` (the sprite: one `<symbol id="p-…" viewBox="0 0 48 48">` per product class — enzymes, pudding, footbath, spray, glass, kidmask, detergent, bar, mask, meat, pina, soda, cart, shelf — drawn in the BOLD MONOLINE rules of `usuals-icon-styles.html`'s style-02 block, Nick's pick of 2026-09-06 (`stroke:#141414; stroke-width:2.5; round caps; one --soft patch`)) and `route-build.mjs` on `js/pearl-usuals.js` (the `use` render) · **Checker:** sonnet
**Files you may touch:** `js/pearl-usuals.js`, `css/pearl-usuals.css`, `img/usuals-icons.svg`. **Never** the concept pages (source of the paths only for the sprite builder's brief: the fourteen `<symbol>` paths are copied from `idea-4-the-usuals.html` lines 187–200 and re-styled — the brief carries the paths, never the page).

**Do exactly this:**
1. Every usual renders only the svg `use` with the symbol from the keyword table (`p-cart` when no keyword matches); no picture is ever requested and no `img` is created.
2. The sprite is fetched once by the browser as `img/usuals-icons.svg` (in `sw.js` ASSETS since STEP 8) and referenced by `use[href="img/usuals-icons.svg#p-…"]`; the tile's `object-fit: contain`, `aspect-ratio: 1`, radius from the drawing.

**PROOF:** `--usuals-states` prints `tiles: N · pictures: 0 · drawn: N · blank: 0` (N derived from the rendered strip) at both viewports, and `states: 5/5 reached · strings verbatim: K/K` (K = the count of the `strings` block, read at run time — nine on 2026-09-06); inside STEP 14.7, the same line on the LIVE URL; every `use` href resolves (the check reads the sprite and asserts each referenced id exists). FAIL: a blank tile in any state, a picture request of any kind, or a symbol id that the sprite does not carry.
**ARTIFACTS ON DISK**: save `usuals-step12-pictures.txt`
**If it fails:** a product name whose keyword the sprite has no symbol for → `p-cart` is the fallback and the run still prints `blank: 0`, a PASS by construction; the missing symbol is recorded in the state file and added in the same builder round.
**Checker's job:** re-run `--usuals-states` yourself; open the sprite and count its symbols against the keyword table.

### STEP 13 — The phone strip, one-handed
**Enter this step when:** STEPS 9–12 are closed.
**Builder:** sonnet (measurement) · **Checker:** glm
**Files you may touch:** evidence `usuals-step13-phone.txt`, `STATE-PEARL-USUALS.md` (a handoff line if the chrome overlaps the strip). **Never** `js/pearl-nav.js`, `css/pearl-nav.css`.

**Do exactly this:** `--usuals-phone` at 375×812 (a real touch swipe of 200 px on `.pl-ustrip`, every `.pl-uadd` ≥ 44 px tall, `document.documentElement.scrollWidth <= clientWidth`, the strip's `scrollbar-width: none` computed) and at 1024×768 (`.pl-rail` — the desktop navigation menu, the Home lane's — absent, `.pl-memory` absent, `.pl-usuals` present in its phone layout); then the thumb-zone read: the strip's top edge sits below the Add card and above the first list at 375 (painted tops ascending: Add card → strip → first list); the phone tab bar never covers the strip's last tile when the page is scrolled to the strip. Any chrome overlap → ONE dated line in the state file's Handoffs to the Home lane, never fixed here.

**PROOF:** `375: swipe scrollLeft>0 ✓ · targets ≥44 ✓ · page scrollWidth<=clientWidth ✓ · 1024: rail absent · timeline absent ✓` and the ORDER anchor for the phone stack prints `0 differences`. FAIL: a horizontal page scrollbar at 375, a target under 44 px, or a strip a thumb cannot move.
**ARTIFACTS ON DISK**: save `usuals-step13-phone.txt`
**If it fails:** the band is this lane's (fix in `css/pearl-usuals.css`, one builder round, the re-check of the named line only); the chrome is not (handoff only). STEP 14 needs this SUCCEEDED.
**Checker's job:** re-run both measurements yourself; swipe by hand through the rig once and read `scrollLeft`.

### STEP 14 — PUBLISH (the design fidelity gate, four items)
**Enter this step when:** STEP 2's revision is APPROVED by Nick (his words in §2d), STEPS 4–13 are closed.
**Builder:** glm (build + deploy commands) · **Checker:** sonnet (re-runs the check on the live URL, three times) · **Design QA:** fable — Sienna (the side-by-side PNGs, full-length captures at both widths) · **Verifier:** the `verifier` agent (re-runs the check and clicks through)
**Files you may touch:** under the round-10 evidence folder: `usuals-fidelity-live.txt`, `usuals-side-by-side-375.png`, `usuals-side-by-side-1280.png`, `usuals-publish.md`; STEPS; `STATE-PEARL-USUALS.md`. **Never** any app file (a defect goes back to its step).

**Do exactly this:**
1. Push the branch to main FIRST (the two-publishers-one-counter lesson): `git fetch origin`, `git rebase origin/main` (never merge, never stash), push the branch with `--force-with-lease`, then fast-forward main to the branch through the repo's normal route (a push of the branch ref to `main` only when `git rev-list --count origin/main..HEAD` shows the branch strictly ahead and the closed plans' files are untouched — the deploy tool refuses a tree behind origin/main anyway). Derive the cache number as max(main's `const CACHE`, the live `sw.js`'s) + 1 and re-set it in `sw.js` if the STEP 8 number is no longer the maximum (a scoped commit, the parity gate re-run).
2. `node projects/personal/family-app/build-dist.js`; `node projects/ops/skippy-jobs/_test-family-app-asset-version-parity.mjs` → PASS; `node projects/ops/deploy.mjs deck-family`; record the deployment URL and the commit hash (`git merge-base --is-ancestor <hash> origin/main`); the wall check: `curl -s -o /dev/null -w '%{http_code}' https://family.heroesandsidekicks.io/api/finances` with no cookie prints `401` (a `200` is a FAIL that stops the step and rolls back through the same tool).
3. Served-bytes assertion: `curl -sL` of `css/pearl-usuals.css?v=N`, `js/pearl-usuals.js?v=N`, `img/usuals-icons.svg`, `sw.js` from the live domain, each `shasum -a 256` equal to the tree's file, content-type asserted (`text/css`, `application/javascript` or `text/javascript`, `image/svg+xml`) — never the cache number or the deploy tool's success line.
4. Fidelity, live: the suite with `--only header,toolbar,lists,layout,usuals,timeline --order --out <the evidence folder>/usuals-fidelity-live.txt --shot <the evidence folder>` at 375×812 and 1280×662, signed in as Nick with the cookie planted before the first load, on `https://family.heroesandsidekicks.io/#shopping` with cache disabled through CDP → two count lines `mismatched properties: 0 · unmeasured anchors: 0 · order: 0 differences`; `--fence-pair off` at `?skin=field` prints 0; the retired-colour sweep prints 0; run three times, all three at zero.
5. Side-by-side PNGs (target left, live right, same width, labelled, FULL-LENGTH captures — never a viewport crop) at both viewports, from `--shot` and the published design page.
6. Sienna grades the PNGs (taste only, now that the count is zero; the six creative gates; a full-length phone capture) → her verdict line in `usuals-publish.md`.
7. The verifier re-runs item 4 first-hand, then `--drive usuals` with a fresh date suffix (adds one test item through a tile, removes it through the app's own check-off), and reads the live usuals against STEP 1's derivation (re-pulled) — Order via Skippy is NOT clicked → its verdict line. THIS IS THE ONLY DEPLOY IN THE PLAN: the live halves of STEP 5 (the U22 read-back) and STEP 12 (the live tiles line) run here, first-hand by the checker, and are pasted into those steps' STEPS entries; a red in any of them is one builder round back to that step and a second run of this step from item 1.

**PROOF:** the four items in `usuals-publish.md`: the two zero-count lines with the evidence file name · the two PNG file names · Sienna's verdict · the verifier's verdict; the deploy hash reachable from origin/main; the anonymous `/api/finances` curl printed `401`; the served-bytes sha256 lines equal to the tree. FAIL: any non-zero count, any missing item, any served asset whose bytes differ from the tree — whatever the screen looks like to anyone.
**ARTIFACTS ON DISK**: save `usuals-publish.md` · save `usuals-fidelity-live.txt` · save `usuals-side-by-side-375.png` · save `usuals-side-by-side-1280.png`
**If it fails:** a non-zero count names its anchor and property → back to that anchor's step for ONE builder round; the re-check is of the named rows only; a served-bytes mismatch → a concurrent lane published from main between items 1 and 2: re-run item 1, republish, never bump the number by hand.
**Checker's job:** re-run the live check yourself, three times, into your own folder; do not accept the builder's paste; your numbers are the ones cited.

### STEP 15 — Blind check of every §2 row
**Enter this step when:** STEP 14's four items are landed.
**Builder:** sonnet as `se-blind-checker`, briefed with §2 verbatim, the signed map, the accent-site file, the measured sign-in route (POST `/__gate`, fields `pw`/`identity`/`next`, the vault entry read at run time, the cookie planted before the app's first load), and told to REFUTE · **Checker:** glm (confirms the report cites each row and voids any red the brief itself narrowed)
**Files you may touch:** NEW `usuals-blind-check.md` under the round-10 evidence folder.

**Do exactly this:** for every LIVE §2 row — U1–U23 less the two retired 2026-09-06 with STEP 6 (U10 and U23), so 21 rows — on the live URL, as Nick and as Chantelle (U21), drive each interaction (U4/U18: add `pearl-usuals-check-<today>-<run id>-blind` through an injected tile, confirm the row lands, check it off through the app's own control; U22: read the board back by id for Status and Bought on) and record PASS/FAIL with evidence (the computed value, the string, the navigation, the response header); compare the rendered usuals (names, order, "last bought" days) to a fresh run of `tools/usuals-pull-history.mjs --print` and record the diff (0 lines is the pass). Order via Skippy is NOT clicked — record it present, enabled and wired.

**PROOF:** `21/21 PASS`, or the named failures. FAIL: any row the checker could not reach (that is NOT MEASURABLE, and keeps the row open).
**ARTIFACTS ON DISK**: save `usuals-blind-check.md`
**If it fails:** failures go back to their step for one round; the re-check covers the named rows only; a red whose expectation is narrower than the map or the site list is voided by the checker's checker, never sent to a builder.

### STEP 16 — Record and close
**Enter this step when:** STEP 15 prints 21/21.
**Builder:** sonnet · **Checker:** glm
**Files you may touch:** this plan (STEPS, SUMMARY, the postmortem section), `STATE-PEARL-USUALS.md`, `PLAN-CHANGES-PEARL-USUALS.md`, `projects/ops/sp-sec/PLAN.md` (ONE appended line), `.claude/skills/plan/references/failure-registry.md` (append only, four-column format, at its REAL path — never through a symlink), and a proposed `projects/personal/family-app/PROJECT.md` status paragraph handed to Nick as one plain sentence (governed — never filed as a ticket while he is asleep).

**Do exactly this:** paste two independent `VERIFIED:` lines per step; write the SUMMARY in plain words; write `## Postmortem` (what the count caught that eyes passed, what the ORDER anchors caught, what a cheap run could not do, the resize measurement, one registry entry or "nothing worth extracting"); no sp-sec line is owed — the server-side outbound fetcher (STEP 6) is RETIRED 2026-09-06 and nothing this plan builds reaches the network from the server, so nothing security-shaped was seen; post `STEP 16 closed <date>` into `PLAN-CHANGES-PEARL-SHOPPING.md` (the parent) as a courtesy line; hand Nick the PROJECT.md sentence.

**PROOF:** `python3 projects/ops/agents/check_plan.py --progress projects/personal/family-app/PLAN-PEARL-USUALS.md (this plan file, CREATED BY STEP 2's first commit)` prints the derived figure; `--artifacts` reports nothing MISSING or EMPTY; the postmortem heading exists. FAIL: a typed percentage, or a registry append committed through the symlink (the checker runs `git ls-files -s` on the real path).
**ARTIFACTS ON DISK**: save `usuals-check-plan-progress.txt`

## 4 · Regret Check (every registry entry, or the plan is not done)

*One row per registry entry, in registry order (PLANNING → DECOMPOSITION → EXECUTION → INTEGRATION → QA → REPORTING → the retro blocks, 189 entries on 2026-09-06), then four NOVEL rows. "Steps" are this plan's §3b steps.*

| Failure mode (registry entry) | The measure in THIS plan that prevents it | Where it lives |
|---|---|---|
| A second system was built because the first was invisible | §0 cites the closed Shopping plan, the family app's estate and the ownership registry row; the generator, suite, map, evidence folder and KV namespace are EXTENDED, never duplicated | §0, §1 anti-scope |
| A capability was declared impossible from a stale or unverified claim | Every "cannot" (no history endpoint, nothing fetches pictures) is re-measured or names the `ls` and the greps that returned nothing: STEP 1 measures what the board actually returns | STEP 1, Already true |
| An absence was asserted without opening the store that would hold it | §Z binds every negative: the "no history endpoint" and "nothing fetches pictures" claims name the `ls` and the two greps that returned nothing | Already true |
| A known constraint's reason was lost, and it silently capped the product | Each frozen constraint names its reason inline (why the URL never comes from the request, why the timeline is desktop-only, why `js/app.js` is untouched) | §3 contracts |
| An instruction assumed capacity the executor doesn't have | Steps are sized to one cheap run each; the suite and the two big files go to opus because the typist refuses them, recorded | §3b standing rule 6 |
| Expectations/manifest rows carried no grounding | Every §2 row cites its hook or its string's source (`ground-truth-usuals.json` `strings`, the app's own ids) | §2 |
| Work was written to a queue no reader ever visits | Every artefact names its reader: the ground truth → STEPS 2, 7, 15; evidence → STEP 14's four verdicts; handoffs → the named plan files | §5 artefact consumers |
| A detector's death was invisible because only its target read it | The suite is run by the builder AND re-run first-hand by a different-session checker, three times at publish; its exit code is asserted | STEP 4, STEP 14 |
| A decision settled once re-opened elsewhere, or two copies of a rule disagreed | THE HISTORY RULE is written once in §3 and implemented once (`plUsualsDerive`), imported by the pull tool and the client; the query string exists once | §3, STEP 7 |
| A rule constraining the user turned out to be an agent's invention | Every rule here carries Nick's quoted, dated words or a named author and date (the DEFAULTED rows name the overseer/the concept) | §1a |
| Remediation was ordered with diagnosis last | STEP 8's first action is the is-it-already-fine check (wired in, nothing composed → fence 0) before any composition | STEP 8 |
| A document, label, or comment was believed over the live system | The board is read by running the query (STEP 1), the live app by the signed-in suite; no comment or concept string is data | STEP 1, STEP 2.3 |
| A proposal was sold on a capability never opened and read | `loadShopping` by bare name, `SHOP_ITEMS_BY_PANEL`, `change_multiple_column_values`, the KV binding, `extraFrames` — each opened and cited by line before being leaned on | Already true, §3 |
| A cause was named and acted on without eliminating alternatives | A failing count is diagnosed per anchor by the suite's own per-property and per-order lines, never by one guessed cause | STEP 14 |
| The human was asked a question the record already answers | The standing-auth audit was read: sign-in, driving the app, testing as Nick are granted; the Monday credential is in the vault, so the column is created by an agent; the sheet asks only the seven V1 rows | §0, §1a, STEP 5 |
| A spec and its guard were authored by the same hand and ratified the same defect | The anchor rows are written by a sonnet builder and SIGNED by Sienna with the counts; the blind checker is briefed to refute | STEP 3, STEP 15 |
| Session rules never reached the subagents doing the work | Every dispatch pastes the MACHINE RULES substance and the file fence; inheritance is assumed to be zero | §5 dispatch header |
| One rule was blanket-applied across items needing per-item answers | Per-tile states (linked, unlinked, on-list, failed picture) and per-state rows are answered one at a time by `--usuals-states` | §2, STEP 12 |
| Pattern-matching scoped too loosely produced false connections | Every anchor's live selector matched exactly one node on the published page (STEP 3's counts file); the keyword table maps by named keyword, unmatched → `p-cart` | STEP 3, §3 sprite |
| Rules existed but were psychologically dormant at answer-time | STEP 0's five-minute loop re-fires the North Star, fan-out, cheap and blocked questions | STEP 0 |
| A run exceeded its cost/time ceiling or hung unbounded | The history pull's activity-log paging is capped at 20 pages and the cap is printed; every cheap run carries the lane's timeout and read cap; a run that exceeds it is split, not retried bigger | STEP 1, §5 |
| A helper was dispatched on a brief with a wrong or missing constraint | Every brief names its file fence, its proof and what must NOT change; the concept pages are named REJECTED in every brief | §3b, §2d |
| A claim about the user/system was made without its source | Every claim about the app cites file:line or the ground-truth entry; every claim about the board cites STEP 1's pull | Already true, STEP 1 |
| A conclusion was drawn from a partial read | STEP 1 pages the Done items until the cursor is null and records the page count; the checker recounts with a second query shape | STEP 1 |
| A fact was quoted as current without its date | Every live value quoted carries its pull date and the command that re-measures it | Already true, §1 |
| A computed value never reached the persistent record | The derivation is written to `ground-truth-usuals.json`; the column id to the state file; every count to an evidence file | STEP 1, STEP 5, STEPS |
| A missing lookup key fell back silently to a wrong default | A usual whose store maps to none shows "no store yet" and never posts; a date with no source is recorded as `updated_at` by name; a selector matching nothing is UNMEASURED, exit 2 | §3 (f), STEP 1, STEP 4 |
| A hardcoded identifier broke when the referent was recreated | The Bought-on column is found by TITLE at creation and its id recorded once; the board and column ids the app already hardcodes are reused, not re-typed | STEP 5 |
| A placeholder or wrong-level path shipped as a literal instruction | Every path in this plan was listed on disk at write time; STEP 2.8 greps PROOF blocks for placeholders | §0, STEP 2 |
| A UI reported success while the backend silently failed | The add is verified by the board read-back and the refetched row, never by the button's state; the check-off by the board's two columns | STEP 10, U22 |
| Mid-session state was assumed unchanged | STEP 14 re-runs every acceptance check on the live URL after the push-to-main; every re-check is a fresh run | STEP 14 |
| Uncertainty was silently absorbed instead of marked | Every evidence line declares its state (ARTIFACT SAVED · NOT MEASURABLE FROM HERE · UNPROVEN); STEP 5 records the column id as a real id or an explicit null | §A states, STEP 5 |
| A serial multi-step operation blew its time budget | The history fetch is three queries with cursor paging; the publish runs the check once per viewport with each count written before the next | STEP 7, STEP 14 |
| An external action went unlogged and became unrecoverable | The column creation prints and records its id; every deploy is logged by deploy.mjs and its hash pasted; every board write in tests names its item id | STEP 5, STEP 14 |
| A tool's own description contradicted house reality and won | Where a tool's text contradicts house reality (the routing hook's refusals, the typist's size/label refusals) the plan names the house fact and the workaround | §0 hook notes |
| Personal/identifying data exposed, or a record written to the wrong subject | No money value, credential or login appears in any brief, ground truth or evidence file — STEP 1.3 masks money values and the proof greps for them | STEP 1.3 |
| One instance of a defect class was fixed while its siblings stayed broken | A defect on one tile kind (linked / unlinked / on-list / skeleton) is fixed for all four in the same step; the suite forces all four | STEP 9, STEP 12 |
| A read operation mutated state | STEP 1's pull is read-only by construction (the proxy refuses `mutation`; the tool is grepped for the word); the suite's drive proof is the only writer and it names its test item | STEP 1, STEP 10 |
| The three biggest absence-claims variants: empty result, broken probe, discarded stderr | Every "not found" is paired with the exact command and a second, differently-shaped search; stderr is captured to the evidence file | Already true, §Z |
| A generated mirror was hand-edited, or its generator never re-ran | `shopping.html` is generated; the published copy is proven byte-equal by sha256; the sprite is built once and referenced | STEP 2 |
| Deployed config silently diverged from source config | The deployed asset bytes are proven equal to the tree by sha256 from the live domain, content-type asserted; the parity gate runs before every publish | STEP 14.3 |
| A delivery path was reordered and its notification behavior changed | N/A: no delivery path or notification is reordered by this build | — |
| A critical boundary was config-editable and could be silently widened | N/A in this plan: the photo route is retired (PLAN-CHANGES-PEARL-USUALS.md 2026-09-06); the measure returns with it; the one boundary this plan still writes — the check-off's two column ids — is hardcoded server-side and the diff is counted hunk by hunk | STEP 5 |
| A "growing" archive had actually frozen | N/A in this plan: the photo route is retired (PLAN-CHANGES-PEARL-USUALS.md 2026-09-06); the measure returns with it; what this plan still dates — the history — is a fresh pull every time and nothing claims freshness it cannot show | §3 THE HISTORY RULE |
| Files were archived but their citations kept pointing at them | The concept pages this plan cites live in the lane's branch under their named folder; every citation was listed on disk 2026-09-06 | §0 |
| A pipeline broke silently and looked identical to a working one | The suite exits non-zero on any mismatch, unmeasured anchor or order difference; a tile whose sprite symbol is missing counts as blank and goes red | STEP 4, STEP 12 |
| Output was delivered somewhere the intended reader never looks | Evidence lands under the round-10 evidence folder as `usuals-*` and the names are pasted in STEPS | STEPS |
| Concurrent sessions clobbered each other's work in a shared file | New files for this lane; the three shared files edited in ONE step with re-read-before-write and a scoped commit, not in another lane's hour; the generator's shared header edited one line only | §3, STEP 8, §W |
| An enforcement gate covered fewer paths than its rule, or failed open | The retired-colour sweep and the fence run over EVERY rendered element, never a sample | STEP 4, STEP 8 |
| Identity or authority was read from a value the caller supplies | The suite's identity comes from the gate cookie; the add posts with the session cookie and `shopping-add.js` re-derives the actor server-side (lines 88–98) | STEP 10 |
| A new failure state was detected but reached no human | A red count, a red parity gate or a 200 on the wall check is a FINISH-LINE failure written into STEPS and the state file; the loop's BLOCKED question surfaces it | STEP 0, STEP 14 |
| The builder graded its own work and passed it | Builder and checker are different models in different sessions on every step; the four publish verdicts come from four different agents | §3b, STEP 14 |
| A check existed that could not fail | STEP 4 proves the property check AND the order check go red on demand (a 1 px padding, a swapped pair); the unit tests are run red first | STEP 4, STEP 5, STEP 7 |
| The review didn't cover the shipped artifact | The checker re-runs against the PUBLISHED URL with served-bytes proven equal to the tree, never the working tree | STEP 14 |
| A narrowing/refactoring change broke the cases that were already correct | The closed plan's anchors #1–#43, #55 are re-measured at zero in every run of this plan's suite; `--fence-pair off` proves the everyday app untouched | STEP 9, STEP 14 |
| A check's verdict depended on wall-clock, machine load, or a concurrent writer | The derive tests inject `now`; the suite pins its viewport and waits for the list to render; "last bought · N days ago" is asserted against the same clock the suite pins | STEP 7, STEP 4 |
| A test existed but nothing ran it | Every proof command is wired into the STEPS section and re-run by the checker; the two unit tests are named in the step map's DONE-PROOF | §3b |
| An interactive element or view shipped untested / unseen | Every §2 interaction is driven on the real surface by the blind checker (tap, swipe, add, check-off, the three curls) | STEP 15 |
| Coverage was reported optimistically | Coverage = verified ÷ the §2 LIVE row count (21 — U10 and U23 retired 2026-09-06 with STEP 6) pinned at plan time; the number reported is `--progress`'s derived figure | VERIFICATION, STEP 16 |
| A staleness/freshness check used the wrong proxy | Freshness of the history is a fresh pull, never the file's mtime; the blind check re-pulls and diffs the live strip against it | STEP 15 |
| A quantitative claim shipped without its method | Every count states its method (the suite's rows, `command grep -c`, the pull tool's summary line with its query) | STEPS proofs |
| Done was declared before the live surface was checked | DONE requires the published URL measured at both viewports, signed in, after deploy, three times, plus the blind click-through | STEP 14, STEP 15 |
| A biometric/metric overrode the human's stated reality | N/A: no biometric or human-state data is rendered | — |
| A correlation was asserted as a cause | N/A: no causal claim is made; the interval is a median of measured gaps, labelled as such | — |
| A nuanced reality was collapsed into a clean binary | Tile kinds, date sources (bought-on / activity / updated_at), picture outcomes (hit / miss / none) and the band are each their own row, never collapsed | §2, STEP 1 |
| A recommendation repeated something already tried, uncited | N/A: no recommendation about Nick's body or money is made | — |
| A wrong record was disclaimed instead of corrected | A wrong evidence line is corrected in place with git keeping the old text; never disclaimed | §A |
| Open items were re-typed from memory and drifted | Open items live only in STEPS and the state file, rewritten in place | STATE FILE, STEPS |
| A deliverable was referenced instead of delivered | Every deliverable is a named file on disk pasted into STEPS; Nick gets the address, never a path | STEPS, STEP 2.9 |
| A report used names/shorthand only the writer understood | SUMMARY and every message to Nick use plain words: what he sees, at which address; no codenames | SUMMARY |
| Commands were sent to a surface that can't run them | Commands for Chrome run through the shared rig; cheap-lane briefs are single-file; nothing is sent to a surface that cannot run it | §5 |
| A number was published without the population it was counted over | Every number carries its population (anchors of N, live §2 rows of 21, tiles of N, Done items of N over a date range) | STEP 1, STEP 14 |
| A finding existed only in the session's output and died with it | Every finding is written to the evidence folder or the state file in the same turn | §5 |
| The plan named a target with total precision, and the target was wrong | The SURFACE row is confirmed by Nick's own words on the LIVE Shopping screen, not by opening the URL | §1a row 1 |
| The human approved a summary, and the summary was silent on the deciding variable | The sheet is GENERATED from §1a by `render_sheet.py`; the deciding variables (icon style, photos, count, phone timeline, no-confirm, till-roll) are its rows | §1a |
| A project stated its scope and never its anti-scope, and lanes leaked into adjacent work | NOT in scope lists eleven things with reasons; the trip-over protocol names where a finding goes | §1 |
| A new rule was written as prose inside its own fix, with nothing enforcing it | The rules that matter are enforced by scripts: the suite's modes, the two unit tests (STEP 5's and STEP 7's), the parity gate, `check_plan.py` | STEP 4, STEP 5, STEP 7 |
| A confirmation was satisfied by checking the wrong kind of fact | V1 rows are confirmed by Nick's words about THIS section; V2 facts by opening the file at the cited line | §1a |
| A blocker common to every lane was carved out of all of them and given to nobody | The chrome has a named owner (Home lane); the closed Shopping layer has its plan; each gets a handoff line, never nobody | §1 anti-scope, STEP 13 |
| Lanes were built to stop: one pass, land, idle — while fixed ceremony ate the context | Steps name their next unblocked step on failure; the loop keeps the queue full; STEPS 1, 5, 6 run in parallel from the start | STEP 0, §3b |
| A caveat nobody measured travelled as fact through multiple independent lanes | Inherited caveats (the typist's refusals, the rig's capture quirks, the proxy's limits) are re-measured by the step that relies on them | STEP 1, STEP 4 |
| The environment destroyed work silently, and the lane wrote a wrong lesson from it | Scoped commits, no stash, re-read before write, step-numbered snapshots, the checkout probed writable at STEP 2 | §W, §3b standing rules |
| A specification described ONE lifecycle in several places, and the copies drifted independently | THE HISTORY RULE, the Bought-on contract and the drawn-asset sprite contract are each written ONCE in §3 and cited by name everywhere else | §3 |
| A task brief on an existing project was treated as the plan, and a generated status checklist was treated as the task list | This PLAN file is the plan; the STEPS section is generated state; the overseer's brief is cited as a brief and deviated from with a reason | header, §0 |
| A regression test's "red-proof" failed for a reason unrelated to the thing it claimed to prove | STEP 4's red-proofs run the unsabotaged control first (`green-proof: 0`, `green-order: 0`) and name the property or container that went red | STEP 4 |
| A standing instruction to route work to an outside/cheap engine eroded over a long session into doing the work directly | The loop's CHEAP question re-routes building every five minutes; opus is named only where the typist refuses or a credential is held, and each refusal is recorded | STEP 0, §3b rule 6 |
| A plan's own second line named a different document as the authority, and the reader proceeded without opening it | The authority line names the closed Shopping plan and the design system, both opened and cited by line in Already true | header, Already true |
| A live bug got three consecutive confident wrong-or-unproven diagnoses, two claiming live verification | A live defect gets one diagnosis by running the suite, whose per-anchor rows are the evidence | STEP 14 |
| Fourteen guards stayed green all day while the live screen showed the wrong thing | Green gates are not the finish: the live count, the served bytes, the side-by-sides and the blind click-through are | FINISH LINE |
| An agent was accused of fabricating its report because a narrow search failed to find the file it cited | A cited file is looked up two ways (path and suffix glob) before anyone is called wrong | §Z |
| A tool's failure verdict was believed without checking the disk — and separately, a success verdict shipped a syntax error | Every tool verdict is confirmed on disk (`node --check`, the diff hunk count) and on the live URL | STEP 5, STEP 14 |
| A build with several independently-shippable pieces was planned and run as one monolithic project | §1b: a single subproject with a stated reason; the parent and siblings named | §1b |
| A rule written only in prose, with no template slot and no machine gate, behaved as if it didn't exist | The rules here have slots: the §2d block, the STEPS proof lines, the STATE file; `check_plan.py` gates the shape | §0, §2d |
| A row-quality check counted TOTAL filled cells instead of checking the specific columns it claimed to require | Proof columns name the specific property, order or string compared, never a filled-cell count | STEP 4 |
| Three independent readers reported wildly different "% complete" for the exact same objective state | Progress is the derived figure from `--progress`; no session types a percentage | STEP 16 |
| A V2 "opened it, here's what I saw" confirmation was wrong three separate times because it opened the WRONG PATH | V2 facts open the exact live file at the cited line in the worktree after the rebase; the concept pages' location was listed, not assumed | Already true |
| A shared coordination file used by several subprojects at once had no per-subproject write fence | This plan has its own STATE and PLAN-CHANGES files; the shared `gen.mjs` header is one appended line; `PLAN-CHANGES-PEARL-SHOPPING.md` gets courtesy lines only | §3, STEP 2 |
| The single cheapest, most decisive test of a build's core hypothesis was defined at planning time but not RUN until after most of the build effort was spent | The cheapest decisive test — does the board hold a usable Done history with dates? — is STEP 1, run first, before any drawing or code | STEP 1 |
| A dispatched build agent reported an interim status as its FINAL answer and returned | A builder never reports "in progress, will resume"; a step ends with a proof or a BLOCKED line naming three tries | §BLOCKED |
| A sandbox restriction produced the EXACT error text this same repo's own CLAUDE.md already documents as a sign of a genuinely broken machine | The routing hook's known refusals are named with the fix; a familiar error text is confirmed by mechanism before it is accepted | §0 hook notes |
| A paid external tool (Codex CLI) ran out of its own usage quota mid-build, and the agent switched to running the command directly | Cheap-vendor quota or refusal is handled by rule 6's stated fallback (opus, identical proof, refusal recorded), never a silent switch | §3b rule 6 |
| A card-creation script reported success and its own internal counter incremented, but the card did not actually exist on live re-query | The add and the check-off are read back from the board by item id through a separate query, never from the POST's reply | STEP 10, U22 |
| Three separate, independently-fatal wiring gaps each made the same feature non-functional in a different way, and NONE were caught by a passing build | Every wiring gap (link tag, script tag, sprite, sw.js assets, contract list) is closed in ONE step with one proof that lists all five | STEP 8 |
| A real, deployed code fix did not reach a real user's already-open browser tab, even after a hard refresh | Editing a `?v=` asset and bumping its version in `index.html` are one unit of work; the checker loads with cache disabled and reads served bytes | STEP 8, STEP 14 |
| A correct, intentional, previously-ruled-on design decision was mistaken for a bug because it was checked from only ONE identity's login | U21 checks the screen as Chantelle; previously-ruled decisions (Pearl default, Mint, the hidden launcher, the never-clicked Order button) are quoted with dates | §2 U21, §1 |
| The Updates panel — the actual surface a person opens — is wired to Monday sync data ONLY, so it will read "No updates" FOREVER | The rendered surface (published URL, signed in) is what the checker reads; a KV write or a board write is never taken for the screen | STEP 14, STEP 15 |
| A pure oversight/QA dispatch was refused twice by the WORK-TYPE gate as "unclear" | Checker dispatches carry ROLE: VERIFIER on a verifier-type agent with no write instruction; the builder edits the plan | §5 dispatch header |
| The same brief, past the work-type gate, was then refused by a SEPARATE gate for missing the MACHINE-RULES travel block | Every dispatch header carries the literal MACHINE RULES substance, both ROLE and NICK-ASKED where needed | §5 |
| A fix was drafted, partially applied to disk, and left in a syntactically-valid but COMPLETELY UNVERIFIED state when the tool writing it hit its usage cap | A partially-applied edit is caught by `node --check` on every touched file and the unit tests at the end of every step | §3b standing rule 5 |
| The above fix's failure was found ONLY because a second, genuinely fresh-context pass re-ran the real test live | The blind checker (STEP 15) is a fresh session that re-runs the live test; the builder's pass is never the final verdict | STEP 15 |
| A confirmed, applied data fix was verified as working because it had only been applied to ONE of two live copies of the same data | Two live copies (`?skin=field` / Pearl; source / dist; branch / main) are each checked: fence 0, served bytes equal, branch pushed to main first | STEP 8, STEP 14 |
| A 16-question regression suite meant to catch exactly this bug class had been silently crashing on question 1 | The suite's own preflight (exit 2 on unmeasured, exit 2 on an unknown flag) means a crashing check cannot read as a pass | STEP 4 |
| Two entire bodies of real, load-bearing work had never been committed to git, on any machine | Every step commits with a pathspec in the same turn as its proof and proves the hash reachable | §3b standing rule 3 |
| A request to deepen an existing artifact was answered by re-polishing the context already in hand | "Deepen" means gather: STEP 1 pulls the board, STEP 2 reads the ground truth; nothing is re-polished from this plan's own prose | STEP 1, STEP 2 |
| A gate protecting one specific, highly sensitive file covered some tool surfaces but not others (Bash) | The data wall is enforced in every brief AND by the cheap lane's egress scan; the credential-holding steps name opus explicitly | §3b rule 6, STEP 1, STEP 5 |
| A function parameter's DEFAULT value silently made an entire decision branch unreachable | The derive tests force every branch (weeks vs days, once vs median, store vs no store, cap vs under cap); STEP 5's test forces the red mutation and the green one | STEP 5, STEP 7 |
| A write-then-rename ("atomic write") pattern was used on a file that has a SECOND, independent writer appending rows | The state file has one writer per hour; generated files are written whole by their generator; the registry is appended at its real path | §3, STEP 16 |
| A test suite's own "red-proof" claimed a safety property held without ever actually removing the fix and running the suite | STEP 4's red-proofs are RUN (the swapped pair, the padding) and their output saved; STEP 5/7's tests are run red first | STEP 4, STEP 5, STEP 7 |
| Test files that exercised a shared module's logging path wrote real output into the REAL production log file | Evidence files are written under the evidence folder; the drive proof's only board writes are one named test item per run | §5, STEP 10 |
| An identity verified once, in memory, from a live authenticated source, was designed to be re-derived later from a file | Identity comes from the live gate on every run; the add's actor is re-derived server-side by `shopping-add.js` | STEP 4, STEP 10 |
| A background daemon process registered a global crash-and-exit handler; a later feature fired a promise without a `.catch()` | N/A: no daemon or long-lived process is built; the client's fetches resolve to `{error}` and the route's fetches are awaited inside try | — |
| A build's supersession of one design correctly re-scoped every task and quietly dropped a piece of functionality | The task list is derived from Nick's three asks and the concept's three parts (strip, add, timeline) plus the two data pieces; §1 WHAT IT MUST DO lists all seven | §1 |
| `fs.watch()` on a shared state directory was assumed to be a sufficient delivery trigger, and was not | N/A: no file watcher is relied on | — |
| A plan asserted facts about the repo it never checked — one step named a symbol that travels under a different name; another's file fence named a file that does not exist | Every symbol this plan names (ids, functions, constants, column ids, flags) was found by grep at the cited line in the worktree; every fenced file was listed on disk | §0, Already true |
| The program fixed what was BROKEN instead of building what was ASKED FOR | The North Star is the usuals and the timeline on the live screen, wired in; steps that stop serving it are corrected in place | NORTH STAR, §N |
| A plan passed every gate — well-formed steps, real proofs — and still could not deliver what the user asked for | The FINISH LINE names the user-visible outcome (his eight usuals with pictures, one tap, the timeline, one-handed) not gate passage | FINISH LINE |
| An assistant's first-person account of its own failure was taken as the root cause by every reader, and it was false | An agent's account of its own failure is re-tested by the checker before it becomes a cause | §A |
| Three verifications were real and all three had the wrong SCOPE: verifying a quote is not verifying the claim; verifying a file once is not verifying it now; verifying the code path is not verifying the thing | Each verification names its SCOPE: the count (suite), the order (suite), the click-through (blind checker), the taste (Sienna), the history (recount) — five different claims | STEP 14, STEP 15 |
| An orchestrator's confident relay propagated a wrong conclusion to five sessions faster than any plan could | Relayed conclusions are re-measured on the live URL; a relayed "the column exists" is re-read with `--check` | STEP 5, STEP 14 |
| One writer in three read the same handoff as a gate and serialized nine of fourteen steps behind another chunk's tenth step | Entry gates name the specific artefact needed; STEPS 1, 5, 6 start in parallel; handoffs are informational | §3b Gate to enter |
| Every failure mode of the file-approval machinery was silent | N/A: no approval machinery is built; governed .md writes (PROJECT.md) are handed to Nick awake as one sentence | STEP 16 |
| A governance CLI silently dropped unrecognized flags (exit 0) | The suite exits 2 on an unknown flag (proven at STEP 4); the pull tool and the column tool accept only their named flags | STEP 4, STEP 1, STEP 5 |
| Plan shape existed as convention, not enforcement: plans degenerated into 1,000-line session logs | Plan shape is machine-gated by `check_plan.py`; STEPS is the only state section; history goes to the state file | §0, STEPS |
| A punchlist item condensed to six words pointed its reader at exactly the wrong action | Every STEPS line carries its DEFINITION OF DONE and PROOF verbatim from the STEP block | STEPS |
| A production secret read as SET when its value was EMPTY, and every check agreed with the wrong answer | N/A: no secret is written by this build; the credential-holding tools read the vault at run time and print presence only | STEP 1, STEP 5 |
| The SAME claim, on the SAME evidence, was CONFIRMED by a checker asked to verify it and REFUTED by a checker asked to break it | The blind checker's brief says REFUTE; STEP 1's checker recounts with a differently-shaped query | STEP 15, STEP 1 |
| Reasoning ABOUT a system instead of ASKING it | Every question about the board or the app is answered by running it: the pull, the `typeof loadShopping` read, the served bytes | STEP 1, STEP 10, STEP 14 |
| A hard prerequisite discovered AFTER a decision, with no owner assigned, silently converts a made decision into an unimplementable one | A prerequisite found mid-drive (a column the token cannot create, a hook missing) gets an owner line in the state file the same turn and an ANSWERED/SUCCEEDED rule per step | If it fails lines |
| A relayed instruction is acted on, or held, by whether the RELAY ITSELF could be the attack | Nick's words are quoted verbatim with dates; a relayed pick (the icon style) lands as his words in §1a or stays DEFAULTED | §1a |
| Two independent programs audited themselves on the same night and found the same disease — every instrument reported a state that was not the system's state | Instruments are preflighted (the suite's selftest, the unit tests' red runs) before any verdict | STEP 4, STEP 5, STEP 7 |
| A PROOF block read as complete while still containing its own template placeholders | STEP 2.8 greps every PROOF block for placeholders and fails on any | STEP 2 |
| Real evidence, deliberately destroyed for a good reason, is indistinguishable from evidence that never existed | Evidence is never deleted; superseded runs stay under the evidence folder with their step and date | §5 |
| A capability was ruled impossible on the strength of a query that structurally could not see the answer | The suite queries the DOM directly; a selector that cannot see the element is UNMEASURED, never absent; the activity log is tried before `updated_at` is accepted | STEP 4, STEP 1 |
| The instruments used to verify a UI lie in four distinct ways | Four instruments cross-check the UI: computed styles, DOM order + painted tops, the blind click-through, the full-length side-by-side | STEP 14 |
| A step's entry gate was satisfied and the step still could not run, and the format had nowhere to say so | STEPS 1 and 5 carry RUNNABLE WHEN; a step whose gate is met but which cannot run writes `STEP N BLOCKED — tried a,b,c` | STEP 1, STEP 5, If you get stuck |
| An automated proof's own internal check detected failure and the surrounding pipeline logged success anyway | The proof asserts the suite's exit code AND the printed counts; a cheap run's own warning line voids its success | §3b rule 7, STEP 4 |
| A dispatch gate blocked the exact defensive pattern its own preceding line prescribed | The dispatch header is copied verbatim from §T; a refusal is read for what it matched before the brief is retried once | §5, §0 hook notes |
| A fallback held in place to make a cutover safe was itself the reason the cutover could never succeed | The drawn asset is the tile's only picture, never a fallback that could hide a red count; a red count blocks the publish | STEP 12, STEP 14 |
| An approved instruction was correct when it was approved and harmful by the time it could be delivered | Nick's approval of the target is dated and the revision named; a later redline re-locks a new revision rather than acting on the old one | §2d REDLINE RULE |
| "I fixed the file" · "I deployed it" · "that is what the user sees" are THREE different claims | Fixed / deployed / seen are three proofs: `node --check` + diff, deploy.mjs + served bytes, the live URL measured signed in with cache disabled | STEP 14 |
| In a multi-session build, code read from the working tree is not the state of the system | The working tree is never the source of truth for the live app; every measurement targets the deployed URL; the branch is rebased before every publish | STEP 14 |
| Three successive rounds of fixes each produced an honest, passing proof, and the user's original complaint was untouched | The original complaint ("a sea of negative space", "photos of the products we normally buy") is the FINISH LINE measured on the live screen | FINISH LINE |
| A correct local caution was escalated into a fleet-wide halt across eight sessions on a crisis that did not exist | A local caution costs one line in NEXT; no halt propagates beyond this lane | §S, §BLOCKED |
| An overseer reported two pieces of work as missing because no message about them had reached its inbox | Missing work is confirmed on disk and in git log before it is reported missing | §Z |
| An acknowledgement from the system under test was read as evidence of the outcome | A 2xx from `/api/shopping-add` is an acknowledgement; the outcome is read from the refetched row and the board read-back | STEP 10 |
| An overseer authorized an action by bridging a DIFFERENT ruling of the user's onto the question | No approval is bridged from another ruling; each V1 row quotes Nick on THIS section or is marked DEFAULTED with its real source | §1a |
| An agent, blocked by a safety guard mid-test, offered the user a choice between loosening the guard and accepting weaker proof | A guard that blocks a test is reported as NOT MEASURABLE — PERMISSION NOT GRANTED, never traded for loosening it | §A |
| A fault that repairs itself faster than anyone reports it is invisible to every alarm in the system | The live check runs three times at publish; every intermittent state is forced deterministically by the suite rather than sampled | STEP 14, STEP 4 |
| A relayed approval was acted on as if the work were still outstanding | A relayed approval (the column created, the page approved) is checked against the board/file and git before work is redone | STEP 5 idempotence, §Z |
| An investigator noticed that a metric could not possibly detect what it was being asked to detect, WROTE THAT DOWN, and then built a headline claim on it anyway | A metric that cannot detect what it is asked to detect (a property count for sibling order) is replaced by one that can (the ORDER mode), not caveated | STEP 4 |
| An investigation's own searches and relays contaminated the evidence it was searching for | Searches for a hook are run before any takeover writes it (STEP 1 and Already true before STEP 9); the test item's name carries the date so no run counts another's | STEP 1, STEP 10 |
| Three unrelated lanes in one night each ran an honest check against an intermittent fault and each got a clean answer | Intermittent states (loading, failed query, failed picture) are forced deterministically with network blocking, not sampled | STEP 4 |
| An overseer holding the user's GENUINE first-hand instructions relayed them as authority to four sessions | Overseer relays carry Nick's quote; no relayed line moves a step without the quote | §5 |
| A file that documents its own version history in prose ABOVE its code turns every unanchored search into a lie | Every grep in this plan is anchored (`^const CACHE`, `^// REV `, a unique token); file histories in prose are excluded from proofs | STEP 8, §1 re-measures |
| A commit hash cited as closing evidence resolved to nothing later | Every cited hash is checked reachable at citation time (`git merge-base --is-ancestor` against HEAD and origin/main) and the message cited beside it | §3b rule 3, STEP 14 |
| A step's own PROOF COMMAND, not just a claim someone else wrote, over-matched | Proof commands name the exact file and a token unique to the change (`'^| [0-9]'` on the map, `mutation` on the pull tool); the checker confirms the token is not matched elsewhere | STEP 1, STEP 3 |
| A check reported PASS five separate times on one feature while the live screen was wrong every time | A check that passes while the screen is wrong is caught by the full-length side-by-side and the blind checker; the ORDER mode is the new instrument for the last such case | STEP 14, STEP 15 |
| A deliberate, reviewed, gate-passing commit was pre-empted by an automatic snapshot that bundled the change with unrelated files | Commits are scoped and made in the same shell as the proof; the shared dependency (the generator header) is changed last | §W, STEP 2 |
| A blind checker's whole verdict came back UNVERIFIED because the route into the walled surface it was handed was a remembered ruling | The blind brief carries the MEASURED route (POST `/__gate`, the vault entry read at run time, the cookie planted before first load) | STEP 15 |
| A scoped restyle rule read correctly, passed its rig and the design QA, and never applied on screen: an inline style set by a frozen script beat it | Every restyle proof is the suite's COMPUTED styles on the live node; the new nodes are this lane's own, so no frozen script writes inline styles on them | STEP 9, STEP 11 |
| A cache-busting parameter placed in the URL hash changed which screen the app believed it was on | Cache-busting lives in `?v=N` and CDP's cache-disable; the suite asserts `data-pl-screen` = shopping before measuring | STEP 8, STEP 14 |
| Three of five blind-check reds were the brief's own narrowing of the pinned design | The blind checker is briefed with §2 verbatim, the signed map and the accent-site file; a red narrower than the pin is voided by its checker | STEP 15 |
| A verification read an eventually-consistent store within seconds of writing it and recorded the stale answer as a product defect | Reads after a write (the add, the check-off, the deploy) wait for the app's own re-render or re-read once before a defect is recorded | STEP 10, STEP 14 |
| A test closed ONE of several identical inputs and read the correct unchanged output as a bug | Drive proofs act on ONE named test row (`pearl-usuals-check-<today>-<run id>`) and assert on that row's normalised name, never on the first of several identical rows | STEP 10, STEP 15 |
| A routing or safety filter matched a keyword in a PARAMETER NAME rather than in any content | Briefs say "credential" and never the tripping word, in parameter names as well as content; a refusal is read for what it matched before the brief is retried once | §0 hook notes, STEP 5 |
| Two cooperating passes wrote the same artifact filename and the richer one was silently lost | Every artefact carries its step and screen (`usuals-step<N>-…`); STEP 14's runs are numbered; the checker writes to its own folder | §3 evidence, STEP 14 |
| A machine owning a whole role went dark, and its peer's CORRECT standby behaviour silently froze 146 scheduled jobs | N/A: this plan runs no scheduled jobs and no standby machine; the drive is a session with a state file a stranger resumes from | — |
| An abandoned merge blocked every commit in a shared workspace for every session, and nothing detected it | Every step's first action is `rev-parse --show-toplevel` and `status --porcelain` on its own worktree; an abandoned merge or a lock file is the step's BLOCKED line with the path | §3b standing rule 1 |
| An append to a SYMLINKED path was committed as the unchanged link, so the content change was never staged | The registry append at STEP 16 goes to the REAL path and the checker runs `git ls-files -s` on it; the plan and state files are real files | STEP 16 |
| A capture instrument reported an element blank while every DOM and computed-style probe said visible | The suite grades by computed styles, DOM order and painted tops; the side-by-side PNGs are for Sienna's taste after the count is zero; a blank-looking tile is first checked by `naturalWidth` and the sprite's symbol list | STEP 4, STEP 12, STEP 14 |
| A cheap vendor re-saved a 40 KB checker file whole twice, and its own proof reverted it both times | The suite is edited on opus by exact hunk (typist refused); every routed edit to the two new files is briefed as an exact hunk and proven by the exact diff | §3b rule 6, STEP 4 |
| A concurrent lane's publish from `main` landed seconds after a branch lane's publish and took the SAME cache number | STEP 14 pushes the branch to main FIRST, derives the cache number as max(main, live)+1, and asserts served bytes against the tree, never the number | STEP 14.1, 14.3 |
| A first-paint acceptance band graded the only correct rendering FAIL, because nothing above the line existed to scroll away | N/A: no clock-bound band; "N days ago" is asserted against the suite's pinned clock and the derivation, with the precondition (a date exists) stated | STEP 7 |
| A failure path shipped untested and failed silently the first three times it fired | Every failure path (U5, U8, a keyword with no sprite symbol) is FORCED once by the suite or a unit test and its output read | STEP 4, STEP 12 |
| A prose section appended through an unquoted shell heredoc executed the backticks in its own text | Prose appends to this lane's plan, state and change log go through the Write/Edit tool or a Python script with literal paths, and the tail is printed back | §0 hook notes |
| Screenshots taken "signed in" showed the signed-out screen | The suite plants the gate cookie before the app's first load and asserts the signed-in DOM before measuring; a page that fails that assertion is NOT MEASURABLE, exit 2 | STEP 4, STEP 14 |
| A checker declared a growing list "settled" after two equal reads 700 ms apart and graded a missing item as a product defect | The suite waits for the strip to RENDER (the skeleton replaced, or `.pl-unone`, or `hidden` — a condition, not a fixed settle) before any row measurement; "absent today" is a data condition | STEP 4, STEP 9 |
| A build gate that insisted on a symlink into a second repository failed every publish after another lane made the file a tracked regular file | N/A: this lane adds no build gate and requires no symlink; the worktree's local links are noted in the state file, never encoded in a gate | — |
| A screen's design-fidelity gate read `0 · 0` three times per round while the live screen disagreed with the approved drawing on sibling ORDER | Four sibling-ORDER anchors in the map (STEP 3), an `--order` mode that prints DOM order and painted tops per container and fails on any difference (STEP 4), its red-proof on a swapped pair, and full-length captures for every by-eye grade (STEP 14) | STEP 3, STEP 4, STEP 14 |
| NOVEL — the drawing's usuals could be typed from the concept's eight demo names, so a "wired in" strip would be measured against invented data | The drawing reads `ground-truth-usuals.json`; STEP 1 runs before STEP 2; the checker diffs the drawn names against the pull | STEP 1, STEP 2 |
| NOVEL — the picture route is the app's first server-side outbound fetcher, so a request-supplied URL would make it fetch anything | N/A in this plan: the photo route is retired (PLAN-CHANGES-PEARL-USUALS.md 2026-09-06); the measure returns with it — the URL-from-the-board-only rule and the sp-sec line stay written in the retired STEP 6 for the NEXT-list round | NEXT list |
| NOVEL — the drive proof must add a REAL item to the family's live list to prove the tap; a real usual would pollute the history it measures | The proof adds an injected test tile named with the date AND a per-run random suffix (so no two runs share a normalised name), removes it through the app's own check-off, and a single Done occurrence never becomes a usual (≥ 2 rule) | STEP 4.4, STEP 10, §3 (c) |
| NOVEL — the generator is shared with the To-Do and Extras lanes, so a REV bump here moves their header line | The header edit is one appended chain entry; the REV number is derived at run time from the chain's last number; a courtesy line goes to the parent plan's change log | STEP 2 |

## 5 · Topology and roles
- **OVERSEER-AUTHORITY:** none named for the family app in `projects/ops/OVERSEER-AUTHORITY.md`'s CURRENT HOLDER table at write time (the closed Shopping plan measured the same on 2026-09-04) — this lane works under the Pearl screens bucket's own Fable overseer per Nick's 2026-09-05 tiering ("Fable for planning/oversight only, cheaper models build and check"). The four approval classes and the data floor never move on the overseer's word.
- Thread layout: ONE overseer thread (Fable) for the Pearl screens bucket; one worker session per running step, dispatched with the §T header, never idle-waiting; STEPS 1 and 5 open together on day one, STEP 2 the moment STEP 1's file lands.
- Overseer: fable (unsticks, design-QA oversight; never builds, never swarms one finding) · Lane manager: none at this size — the overseer coordinates directly · Workers: glm builders (through `cheap-task.mjs` / `route-build.mjs`), deepseek extractors, sonnet checkers/test authors, opus builders only for the credential-holding tools, the suite, `index.html`/`sw.js` (NICK-ASKED), fable (Sienna) design QA, `se-blind-checker`, `verifier`.
- State files location: `projects/personal/family-app/` — `STATE-PEARL-USUALS.md`, `PLAN-CHANGES-PEARL-USUALS.md` (both created by STEP 2, beside this plan). No QUESTIONS.md/ASSUMPTIONS.md: every open question is a §1a row; assumptions are the DEFAULTED rows.
- **Board card id:** none yet
- **Artefact consumers:** `ground-truth-usuals.json` → STEPS 2, 3, 4 (`strings`), 7, 15; the anchor rows → STEP 4; the suite's new modes → STEPS 9–15; the column id → STEPS 5, 7; evidence files → STEP 14's four verdicts and STEP 16; handoff lines → `STATE-PEARL-USUALS.md`, `PLAN-CHANGES-PEARL-SHOPPING.md` (courtesy), the Home lane's hand-off file if the chrome overlaps the strip. Every raise path is proven to ARRIVE by the receiving file's own line (the checker reads it back).
- **Write-contention:** this lane owns its NEW files outright; the three shared app files are edited once, at STEP 8, by one builder, re-read before write, scoped commit, never in the same hour as another lane's step that touches them (the state file records the hour); the shared generator gets one appended header line at STEP 2; the checkout is proven writable at STEP 2 (probe write, read-back, clean status) and re-proven at STEP 8 and STEP 14.
- **Concurrency:** hard ceiling 8 simultaneously-running agents in this session, machine-wide budget ~40 shared with every live session — measured 6 live sessions on 2026-09-06 (`ls /tmp/cc-socks/ | wc -l`), so this lane's cap is 6; re-count before the first wave and divide; a wave that has not returned is load, not progress. Raising either number is Nick's call, named here, never a session's own.
- **Dispatch header (verbatim, every dispatch):** `ROLE: <GATHERER|BUILDER|VERIFIER|CRITIC|RECONCILER>` · `NICK-ASKED:` only when he named the model · `REVIEW: t2` · `RETURN-SIZE: ~1500 tokens — write findings to disk, return a pointer` · the literal words `MACHINE RULES` with the substance pasted (never a bare `git commit`; never `git stash`; never `git merge`; never `--no-verify`; re-read before write; `command grep`; the four approval classes; the data floor; nobody grades their own work; an empty result is evidence about the search; no security work; no second plan file; Order via Skippy is never clicked; the concept pages are rejected for measurement) · the task, the file fence, the exact proof, the stop conditions. Cheap-lane briefs additionally: repo-relative paths only, no folder listings, no `.md` files, no money values, no credentials, never the word that trips the secret filter (say "credential").

**Per-stage topology — counts DECLARED at plan time:**

| Stage | Overseer | Sub-overseers | Workers |
|---|---|---|---|
| Plan + Design (STEPS 1–3) | 1 | 0 | 4 |
| Tests + Framing (STEPS 4–8) | 1 | 0 | 5 |
| Elements + Details (STEPS 9–13) | 1 | 0 | 5 |
| Output (STEP 14) | 1 | 0 | 4 |
| Proof (STEPS 15–16) | 1 | 0 | 2 |

**The walk-away contract — a stranger resumes the drive from files alone:**
- **STATE FILE:** `projects/personal/family-app/STATE-PEARL-USUALS.md` (created at STEP 2, current-state only, rewritten in place)
- **HEARTBEAT ROW:** pearl-usuals-drive, registered by the drive coordinator in `projects/personal/skippy-app/ala-state/work-threads.json` when the drive opens
- **MORNING-REPORT LINE:** "Pearl Usuals — <n>/21 manifest rows verified, current step, next unblocked step, fidelity + order count on the live URL, the column id state" in `projects/ops/walkaway/REPORT.md`

## 6 · Evals — what "working" means, decided now

| Capability | Check (exact command or procedure) | Pass looks like |
|---|---|---|
| (1) The usuals derive from the board's real Done history, the same way in the pull and in the client | STEP 1's `--check` + STEP 7's derive test + STEP 15's diff of the live strip against a fresh `--print` | `done items: N · … · usuals: M`; `derive: 12/12 PASS · matches STEP 1's saved list: yes · query equals STEP 1's committed query: yes`; blind diff 0 lines |
| (2) The strip and the timeline match the locked drawing, including sibling order | STEP 14's live run of the suite with `--order` at both viewports, three times | `mismatched properties: 0 · unmeasured anchors: 0 · order: 0 differences` ×2 ×3 |
| (3) One tap adds the usual to its store's list through the existing add route | STEP 10 `--drive usuals` and the blind check's U4/U18 | `usuals: add ✓ (row landed, count +1) · on-list ✓ · fail-string ✓ · removed via check-off ✓` |
| (4) Every usual shows a drawn bold-monoline asset, never blank | STEP 12 `--usuals-states` on the live URL after STEP 14 | `tiles: N · pictures: 0 · drawn: N · blank: 0` |
| (5) A check-off writes Done AND Bought on | STEP 5's red/green read-back and the blind check's U22 | the board item reads Status "Done" and Bought on = today |
| (6) The phone strip works one-handed | STEP 13 `--usuals-phone` | `375: swipe scrollLeft>0 ✓ · targets ≥44 ✓ · page scrollWidth<=clientWidth ✓ · 1024: rail absent · timeline absent ✓` |
| (7) Nothing else changed | STEP 8 and STEP 14 `--fence-pair off` at `?skin=field`; the closed plan's anchors in every run | `0 changed elements` at 375 and 1280; anchors #1–#43, #55 still zero |
| The cheapest invalidating test, run first | STEP 1: does the board hold a Done history with dates that yields at least one usual? | `usuals: M` with M ≥ 1 — or M = 0 recorded and the EMPTY state becomes the default drawing, before any code is written |

## If you get stuck (all steps)

Before writing "blocked": (1) try a concrete workaround, (2) re-read the step's proof requirements — most "stuck" is a misread gate, (3) write one line to the overseer AND the owner of the blocker. Only then log `STEP <N> BLOCKED — tried: <a>,<b>,<c>. Need: <one sentence>.` Then keep working every other unblocked step. Never idle on a blocker. The gates that DO stop work: the four approval classes · the data floor · the §S security click · a proof that would destroy live data. Nothing else does.

## Your loop

Every pass: find the lowest-numbered step whose enter gate is proven and which is not yet proven → do it → produce its proof → paste the proof under the matching item in STEPS below → repeat. STEP 0's five-minute loop runs the whole time.

## SUMMARY — a few plain-English lines, read by the status generator

Nothing is built yet. This plan says how the family app's Shopping screen gets a strip of the things the house buys again and again, each with a drawn bold-monoline picture — the icon style Nick picked on 2026-09-06 — and one tap to put it back on the list, and a desktop timeline of when things were last bought, all running on the real check-off history from the shopping board, not a typed list. Real product photos are not part of this build and go on the next list, as does the one-store-at-a-time phone mode. First the history is pulled and checked against the board, then the drawing is extended and published for Nick's yes, then the data pieces and the screen pieces are built and measured to zero differences against that drawing, then it is published and checked by people who did not build it. Nick's only wait is a yes on the published drawing; everything else runs without him.

## STEPS

<!-- The live status checklist, read by status-regen.mjs / project-status-page.py. The heading
     above must be exactly "## STEPS" with nothing else on the line. -->

> One numbered line per STEP block above, same numbers. This section is CURRENT STATE, rewritten in place — the STEP blocks say what to do; this section records what has been proven. The pasted proofs land here as `VERIFIED:` lines. Never let narrative creep in — history goes to `STATE-PEARL-USUALS.md`.

1. [Plan] Ground truth of the history — 0%
   DEFINITION OF DONE: `ground-truth-usuals.json` exists with N Done items, a date range and M usuals; the checker's own recount equals N and its derivation equals the M names in order
   PROOF: `node tools/usuals-pull-history.mjs --check`
2. [Design][UI] Lock the target — 0%
   DEFINITION OF DONE: the extended drawing published at the same address as REV n+1, three sha256 equal, Sienna's six-gate verdict recorded, Nick's approving words in §2d
   PROOF: `curl -sL https://skippy-designs.pages.dev/family-pearl-shopping-r10.html | shasum -a 256` equals the local hash
3. [Design] Anchor map additions, signed — 0%
   DEFINITION OF DONE: rows #56 onward including four ORDER anchors, second SIGNED BY line, K ≤ 2, hash machine-written
   PROOF: `command grep -c '^| [0-9]' projects/personal/family-app/redesign-mockups/concepts-2026-09-04-round10-screens/gen.mjs-anchors-shopping.md` ≥ 78
4. [Tests] Suite additions, red-proofed — 0%
   DEFINITION OF DONE: the five new modes and the new rows; selftest prints the padding red-proof and the order red-proof, then zeros; sabotage names every new property
   PROOF: `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --selftest`
5. [Framing] The Bought-on column and the check-off that sets it — 0%
   DEFINITION OF DONE: the column exists (id recorded) and a check-off through the app writes Done AND the date, read back by id; the function diff is two hunks
   PROOF: `node tools/usuals-bought-on-column.mjs --check`
6. [Framing] The picture route — RETIRED 2026-09-06 — 100% (nothing to build; PLAN-CHANGES-PEARL-USUALS.md)
   DEFINITION OF DONE: nothing is owed — the step is retired; its design is preserved in the STEP 6 block for the NEXT-list round
   PROOF: RETIRED — the dated entry is in `projects/personal/family-app/PLAN-CHANGES-PEARL-USUALS.md`
7. [Elements] The client derivation and the history query — 0%
   DEFINITION OF DONE: `plUsualsDerive` reproduces STEP 1's list; the pull tool imports the same bytes; the query string equals STEP 1's
   PROOF: `node tools/usuals-derive.test.mjs`
8. [Framing][UI] Wire in, painting nothing — 0%
   DEFINITION OF DONE: tags, sw.js assets and cache, contract lists in one change; parity PASS; fence-pair off and on both 0
   PROOF: `node projects/ops/skippy-jobs/_test-family-app-asset-version-parity.mjs`
9. [Elements][UI] The rail and the tiles — 0%
   DEFINITION OF DONE: the usuals anchors zero at both viewports, DOM order = derived order, accent sites appended
   PROOF: `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --only usuals --inject <abs css/pearl-usuals.css> --inject-js <abs js/pearl-usuals.js> --order`
10. [Elements][UI] One-tap add — 0%
   DEFINITION OF DONE: an injected test tile adds a real item, the row lands with the motion, the tile reads On the list, the fail string shows on a forced 500, the app's own check-off removes it
   PROOF: `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --drive usuals`
11. [Details][UI] The last-bought timeline — 0%
   DEFINITION OF DONE: the timeline anchors zero at 1280×662, date-descending order, absent at 375, columns end at 622
   PROOF: `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --only timeline --inject <abs css/pearl-usuals.css> --inject-js <abs js/pearl-usuals.js> --order`
12. [Details][UI] The pictures — 0%
   DEFINITION OF DONE: tiles = drawn assets, pictures 0, blank 0, every sprite symbol resolves
   PROOF: `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --usuals-states`
13. [Details][UI] The phone strip, one-handed — 0%
   DEFINITION OF DONE: swipe scrolls, 44 px targets, no sideways page scroll at 375, the 1024 band phone-shaped
   PROOF: `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --usuals-phone`
14. [Output][UI] PUBLISH — the four-item gate — 0%
   DEFINITION OF DONE: branch → main first; deploy; wall 401; served bytes equal the tree; live count and order zero ×3 at both viewports; the checker's side-by-sides; Sienna's verdict; the verifier's verdict
   PROOF: `node projects/personal/family-app/tools/pearl-fidelity-shopping.mjs --only header,toolbar,lists,layout,usuals,timeline --order --out <abs path of the round-10 evidence folder>/usuals-fidelity-live.txt`
15. [Proof][UI] Blind check of every §2 row — 0%
   DEFINITION OF DONE: 21/21 PASS as Nick and as Chantelle, the live usuals equal to a fresh pull
   PROOF: `usuals-blind-check.md` in the evidence folder contains `21/21 PASS`
16. [Proof] Record and close — 0%
   DEFINITION OF DONE: two VERIFIED lines per step, the postmortem, the sp-sec line, the registry append at the real path, the derived progress figure
   PROOF: `python3 projects/ops/agents/check_plan.py --progress projects/personal/family-app/PLAN-PEARL-USUALS.md (this plan file, CREATED BY STEP 2's first commit)`