{
 "updated_at": "2026-09-12T01:57+00:00",
 "updated_by": "Stream G",
 "delivery": "2026-09-18",
 "cannes": "2026-09-27",
 "note": "Edit this file on the host; the page reads it per request (import/ is mounted read-only into the container). Status values: todo | doing | done | blocked | deferred.",
 "waves": [
  {
   "key": "w1",
   "title": "Wave 1",
   "dates": "Fri 4 \u2013 Sun 6",
   "goal": "decide, unblock, back up"
  },
  {
   "key": "w2",
   "title": "Wave 2",
   "dates": "Mon 7 \u2013 Fri 11",
   "goal": "the heavy build; checkpoint Fri 11"
  },
  {
   "key": "w3",
   "title": "Wave 3",
   "dates": "Mon 14 \u2013 Tue 15",
   "goal": "features on top"
  },
  {
   "key": "w4",
   "title": "Wave 4",
   "dates": "Wed 16 \u2013 Fri 18",
   "goal": "one session: integrate, test, launch checklist"
  },
  {
   "key": "buf",
   "title": "Buffer",
   "dates": "Sat 19 \u2013 Fri 26",
   "goal": "deferred items, late content, Mon 21 human review, Cannes asks"
  }
 ],
 "streams": [
  {
   "key": "A",
   "title": "A \u00b7 Collectors & data",
   "session": "/stream-a",
   "tasks": [
    {
     "id": "A0",
     "wave": "w1",
     "title": "Paris (Extime) collector built and fully collected (7,032 listings)",
     "status": "done",
     "paid": true,
     "due": "2026-09-04"
    },
    {
     "id": "A1",
     "wave": "w1",
     "title": "robots.py: by-name matcher, one policy, crawl-delay [robots.py, 42 tests, 7 collectors on check_allowed; Shopify+Dubai 403 recorded]",
     "status": "done",
     "paid": false,
     "due": "2026-09-05"
    },
    {
     "id": "A2",
     "wave": "w1",
     "title": "Migration #1: accounts skeleton, raw_records, source_kind, identity_mode, vertical, visible=False [0a1b2c3d4e5f; up/down/up on dfp-devdb; backfill accounts idempotent]",
     "status": "done",
     "paid": false,
     "due": "2026-09-05"
    },
    {
     "id": "A3",
     "wave": "w1",
     "title": "RawListing.raw/vertical populated by every collector [raw+vertical on all 7 collectors; ingest writes raw_records (stub run on devdb); backfill vertical idempotent]",
     "status": "done",
     "paid": false,
     "due": "2026-09-05"
    },
    {
     "id": "A4",
     "wave": "w1",
     "title": "read_one() on Avolta, Shopify, ARI, Extime [read_one on Avolta/Shopify/ARI/Extime, refusals on Dubai+Heinemann; 13 tests on a real ATH product page]",
     "status": "done",
     "paid": false,
     "due": "2026-09-05"
    },
    {
     "id": "A5",
     "wave": "w1",
     "title": "Audit defect list (orphan tiles, size veto, Dubai in_stock, MEX currency, FX, images provenance, stuck runs) [defect list + collector skip hook + brand_key; 304 tests; backfills run twice on devdb]",
     "status": "done",
     "paid": false,
     "due": "2026-09-05"
    },
    {
     "id": "A10",
     "wave": "w2",
     "title": "Drinks refresh of the sixteen (minus Dubai), overnight [5 Sep refresh: 10 Avolta stores + Dublin + 4 Shopify, all ok, 2,829 prices; Dubai refused; Paris fresh from 4 Sep]",
     "status": "done",
     "paid": true,
     "due": "2026-09-07"
    },
    {
     "id": "A6",
     "wave": "w2",
     "title": "Beauty widening: per-vertical walk, Dublin prefixes, Shopify Spanish shelves, page-1-only at LHR/ATH/MAD/BCN [3d5f3f1: two trees per Avolta store, beauty page-1 + targeted product pages, held-URL re-read at query-blocked stores, Dublin /beauty/ targeted, Shopify Spanish+beauty shelves, Extime size cross-check on the live shape; 46 tests; LHR shrink measured from tonight's run counters]",
     "status": "done",
     "paid": true,
     "due": "2026-09-08"
    },
    {
     "id": "A7",
     "wave": "w2",
     "title": "Targeted 40 beauty list by Tue 8 (method in plan \u00a77) [fcdb0a7: .logs/runs/beauty-candidates-2026-09-05.md + import/beauty-targets.json; 20 fragrance at 5 stores, 20 skincare at 3-5; Dublin and Avolta page-1 reads pending their refresh]",
     "status": "done",
     "paid": true,
     "due": "2026-09-08"
    },
    {
     "id": "A8",
     "wave": "w2",
     "title": "Migration #3: brands table, size_value/unit, per-vertical attribute, merged_into_id, merge/reverification tables [b46e3f8: f3a4b5c6d7e8 rehearsed up/down/up on dfp_q; models + ingest veto + resolve_product_id; 16 tests + stub run on the dev copy]",
     "status": "done",
     "paid": false,
     "due": "2026-09-09"
    },
    {
     "id": "A9",
     "wave": "w2",
     "title": "Brand fold + uncategorised backfill + 52 duplicate groups as recorded merges [758233e: rederive + backfill brands/sizes/categories/merges rehearsed twice on a clean copy (2,215 brands, 1,032 categorised, 40 groups/44 rows merged, second pass a no-op); resolve_product_id follows merged_into_id; 27 tests]",
     "status": "done",
     "paid": true,
     "due": "2026-09-10"
    },
    {
     "id": "A13",
     "wave": "w2",
     "title": "Switch the nineteen airports on for the public site (after the verify tripwire is green); rian deploys [11 Sep: locations --only the nineteen on production and staging (four non-launch sources hidden). The site is members-only, so nothing public changed. Tripwire: verify --n 20 running on production (two shops in: 39 pass, 1 price moved); if it trips, hide again. Rian's thirty-page check and the bot mailbox stay open on the running list.]",
     "status": "done",
     "paid": true,
     "due": "2026-09-11",
     "rian": true
    },
    {
     "id": "A11",
     "wave": "w3",
     "title": "Two categories at twenty each, across airports",
     "status": "todo",
     "paid": true,
     "due": "2026-09-15"
    },
    {
     "id": "A12",
     "wave": "w3",
     "title": "Full recollection after the widening",
     "status": "todo",
     "paid": true,
     "due": "2026-09-15"
    },
    {
     "id": "A14",
     "wave": "buf",
     "title": "Paco Rabanne to Rabanne as a confirmed brand alias, unless M4 already recorded it: then verify and close the issue",
     "status": "todo",
     "paid": false,
     "due": "2026-09-16"
    },
    {
     "id": "A15",
     "wave": "buf",
     "title": "The six Avolta stores showing our own breakage: diagnosed per store from run history, one announced read each, fixed as tests",
     "status": "todo",
     "paid": false,
     "due": "2026-09-16"
    },
    {
     "id": "A16",
     "wave": "buf",
     "title": "The four Heinemann-family shops that worked and then refused: recorded as refusals with the date, never retried; the partnership ask drafted for rian",
     "status": "todo",
     "paid": false,
     "due": "2026-09-16"
    },
    {
     "id": "A17",
     "wave": "buf",
     "title": "One display rule for sizes (700ml against 70cl), applied in one place; the shop's own wording kept in the record",
     "status": "todo",
     "paid": false,
     "due": "2026-09-17"
    },
    {
     "id": "A18",
     "wave": "buf",
     "title": "Shops and terminals within one airport: the data-shape proposal written and a decide item raised; no migration without rian",
     "status": "todo",
     "paid": false,
     "due": "2026-09-17"
    }
   ]
  },
  {
   "key": "A2",
   "title": "A2 \u00b7 Rendered fetch (browser)",
   "session": "/stream-a2",
   "tasks": [
    {
     "id": "R1",
     "wave": "w1",
     "title": "Playwright sidecar: isolated network, egress allowlist, non-root, floor delay [sandboxed launch verified under cap_drop ALL + no-new-privileges + seccomp; render of a public page and refusal of an IP literal through the hardened container; compose config valid]",
     "status": "done",
     "paid": true,
     "due": "2026-09-06"
    },
    {
     "id": "R2",
     "wave": "w1",
     "title": "fetch.render() with the fetch() honesty contract [tests/test_render.py (27) + live render of example.com through the sidecar via render(); check.sh green]",
     "status": "done",
     "paid": true,
     "due": "2026-09-06"
    },
    {
     "id": "R3",
     "wave": "w1",
     "title": "Shilla probe: one rendered product page; stop if challenged [product page 5581103 rendered HTTP 200, no challenge, server-rendered document; run log .logs/runs/shilla-probe-2026-09-05.log; decisions #11 #12]",
     "status": "done",
     "paid": true,
     "due": "2026-09-06"
    },
    {
     "id": "R4",
     "wave": "w2",
     "title": "Singapore collector on the targeted set [collector built and tested on the real JW Blue record; first run 5 Sep stored 17 prices then 403 on page 19; no retry until rian answers decide-singapore-answered-403-on-the-19th-page-of-its-first-run]",
     "status": "blocked",
     "paid": true,
     "due": "2026-09-09"
    },
    {
     "id": "R5",
     "wave": "w2",
     "title": "read_one() for Changi via render (cap 20/run) [Seoul first run 5 Sep: 18 prices from 20 renders, no refusals]",
     "status": "done",
     "paid": false,
     "due": "2026-09-10"
    }
   ]
  },
  {
   "key": "Q",
   "title": "Q \u00b7 Quality & verification",
   "session": "/stream-q",
   "tasks": [
    {
     "id": "Q1",
     "wave": "w2",
     "title": "app.cli audit (JSON until its table lands) [audit.py + cli audit: 15 metrics, thresholds pinned to 2026-09-05 measurements, 34 tests, first snapshot in .logs/verification/]",
     "status": "done",
     "paid": true,
     "due": "2026-09-07"
    },
    {
     "id": "Q2",
     "wave": "w2",
     "title": "Migration #2: verification_runs/checks, audit_snapshots, rejected_observations [d1e2f3a4b5c6 rehearsed up/down/up on dfp_q (today's dump); rejections + skip_counts + audit_snapshots proven by writes]",
     "status": "done",
     "paid": false,
     "due": "2026-09-07"
    },
    {
     "id": "Q3",
     "wave": "w2",
     "title": "app.cli verify: per host, abort at first BLOCKED, tripwire verdicts, REVIEW [verify.py + verify/verify-status/verify-clear; rehearsed on dfp_q with a stand-in reader (all ten verdicts, block, clear, spot-checked); 47 tests; no real retailer run tonight by rule]",
     "status": "done",
     "paid": true,
     "due": "2026-09-09"
    },
    {
     "id": "Q4",
     "wave": "w2",
     "title": "QUALITY.md in plain language: what verify does and does not prove [QUALITY.md written for rian: what audit/verify prove and cannot, verdict table, tripwire, BLOCKED rule, public labels, first figures]",
     "status": "done",
     "paid": false,
     "due": "2026-09-10"
    },
    {
     "id": "Q5",
     "wave": "buf",
     "title": "Mon 21 Sep human review (5 h), lists emitted by audit",
     "status": "todo",
     "paid": true,
     "due": "2026-09-21"
    }
   ]
  },
  {
   "key": "B",
   "title": "B \u00b7 Pages & SEO",
   "session": "/stream-b",
   "tasks": [
    {
     "id": "B1",
     "wave": "w2",
     "title": "Product page body rendered server-side (headings, price table, awards) [curl of a product page shows the H1 and every price; 16 renderer tests; Lighthouse local mobile: CLS 0.003 to 0, LCP 3.2s to 3.0s, perf 93 both]",
     "status": "done",
     "paid": true,
     "due": "2026-09-05"
    },
    {
     "id": "B2",
     "wave": "w2",
     "title": "\u00a76 corrections: absolute URLs + PUBLIC_BASE_URL, gtin by width, availability, Organization, drop SearchAction, real 404s, HEAD, cache headers, no-store [curl: HEAD 200 no body, If-None-Match 304, unknown id and route 404+noindex, no-store on HTML+JSON, JSON-LD absolute with nested offers; 39 tests]",
     "status": "done",
     "paid": true,
     "due": "2026-09-05"
    },
    {
     "id": "B3",
     "wave": "w2",
     "title": "Structure page half-day sweep before it goes to Mark [rendered /structure checked: no 558, no five airports, no free/almost nothing, SearchAction line aligned, live linked count, beauty counts from the live DB 2026-09-04; one marked placeholder left for rian (s10 #10)]",
     "status": "done",
     "paid": true,
     "due": "2026-09-05"
    },
    {
     "id": "B4",
     "wave": "w2",
     "title": "Airport pages [curl of /airports/lhr-london shows the H1, four counts, cards with prices, CollectionPage+ItemList+Airport JSON-LD, sitemap entry; served root diffed identical to headless Chrome's DOM; 301/404/HEAD/304 pinned; 83 tests]",
     "status": "done",
     "paid": true,
     "due": "2026-09-09"
    },
    {
     "id": "B5",
     "wave": "w2",
     "title": "Brand pages (on the brands table) [brand pages on brands.slug (A8/A9 landed mid-session): /brands/johnnie-walker served root diffed identical to the SPA DOM on dfp_a; alias and case 301, thin brand 404, 96 sitemap entries on the rehearsal copy; CollectionPage+Brand+ItemList; 20 tests; product-page links wait on brand_slug in the product API (request to A)]",
     "status": "done",
     "paid": true,
     "due": "2026-09-11"
    },
    {
     "id": "B6",
     "wave": "w3",
     "title": "Category and category-at-airport pages (per \u00a710 #10)",
     "status": "todo",
     "paid": false,
     "due": "2026-09-15"
    },
    {
     "id": "B7",
     "wave": "w3",
     "title": "IndexNow, per-bot robots, RSS, llms.txt, Dataset page [robots.txt with Content-Signal and per-bot groups from one switch table; /feed.xml RSS; /llms.txt; /data with Dataset JSON-LD (server body diffed identical to the SPA DOM); python -m app.cli_pages indexnow (dry run exercised on the dev copy, no network); 47 tests]",
     "status": "done",
     "paid": false,
     "due": "2026-09-15"
    }
   ]
  },
  {
   "key": "C",
   "title": "C \u00b7 Design",
   "session": "/stream-c",
   "tasks": [
    {
     "id": "C1",
     "wave": "w1",
     "title": "Brand alignment pass one (logo, palette, serif)",
     "status": "done",
     "paid": true,
     "due": "2026-09-03"
    },
    {
     "id": "C2",
     "wave": "w2",
     "title": "Mega menu [c06a766. Verified in headless Chrome on two dev copies (brands present / brands empty): hover, toggle, Enter/Tab/Escape, outside click, navigation, phone drawer sections; 38 panel links all 200; nav row 47.9px in both states; Lighthouse CLS 0.000 product page, 0.009 home. Waits only on B's header_html() mirror (issue filed, blocking) for the same deploy.]",
     "status": "done",
     "paid": true,
     "due": "2026-09-09"
    },
    {
     "id": "C5",
     "wave": "w2",
     "title": "Brand pass two: banners designed into the pages, sponsor sizes decided (then Adam gets the sizes) [11 Sep evening: rian rejected the first placements (IAB boxes dropped between sections read as arbitrary). Slots stay in code, hidden; a design pass makes them part of the layout before sizes go to Adam. Accent picker on /settings kept for Adam to choose. Flags: small flag beside the code (cards, airport hero), flag behind the code on the home picker.]",
     "status": "doing",
     "paid": true,
     "due": "2026-09-09",
     "rian": true
    },
    {
     "id": "C3",
     "wave": "w2",
     "title": "Featured savings and exclusives prominence [6fa56d8 (+1034869 AA gold text). Headline saving card from featured[0], exclusives band with live total, product-page saving enlarged, excl badge tone ready (B flips one word). Verified in headless Chrome 1280/390 on dfp_c; check.sh green.]",
     "status": "done",
     "paid": true,
     "due": "2026-09-10"
    },
    {
     "id": "C4",
     "wave": "w2",
     "title": "Instagram/YouTube links; CSS for Q/R surfaces on request [62ffa7e. Header and footer links behind window.__DFP_SOCIAL__ (server-injected, https only); renders nothing today. B wires the env setting, shell injection and sameAs (issue filed, costly).]",
     "status": "done",
     "paid": false,
     "due": "2026-09-14"
    }
   ]
  },
  {
   "key": "D",
   "title": "D \u00b7 Content",
   "session": "/stream-d",
   "tasks": [
    {
     "id": "D1",
     "wave": "w2",
     "title": "Articles table (kind: article | airport_writeup | category_intro) + import command [commit 89f33a0; migration a4b5c6d7e8f9 rehearsed up/down/up on dfp-devdb; import of md, txt, docx and /todo uploads rehearsed on a scratch DB; 45 tests]",
     "status": "done",
     "paid": true,
     "due": "2026-09-08"
    },
    {
     "id": "D3",
     "wave": "w3",
     "title": "Email capture (migration #5) + subscriber export [commits 000765e + 198d952: POST /api/subscribers throttled with honeypot and consent record, 11 tests incl. no-address-in-logs; owner-only CLI export rehearsed on dfp_d; SubscribeForm component ready, placement is C's (running list)]",
     "status": "done",
     "paid": true,
     "due": "2026-09-14"
    },
    {
     "id": "D2",
     "wave": "w3",
     "title": "Adam's articles published; airport write-ups; category intros [pages and blocks built and served since 0.33.0 (article pages, airport write-up block); waits only on Adam's hand-ins on /todo, none received]",
     "status": "blocked",
     "paid": true,
     "due": "2026-09-15"
    },
    {
     "id": "D4",
     "wave": "w3",
     "title": "Sponsor slots at IAB sizes [commit 3724f38: SponsorSlot for the nine IAB sizes reserves its box, static image only, nothing when no creative; registry in lib/sponsors.ts; positions and placement follow C5 (issue to C on the running list)]",
     "status": "done",
     "paid": true,
     "due": "2026-09-15"
    }
   ]
  },
  {
   "key": "E",
   "title": "E \u00b7 Infrastructure",
   "session": "/stream-e",
   "tasks": [
    {
     "id": "E2",
     "wave": "w1",
     "title": "Nightly pg_dump cron (installed 4 Sep 03:15, 14-day retention); rehearsed restore still to do",
     "status": "done",
     "paid": true,
     "due": "2026-09-05"
    },
    {
     "id": "E1",
     "wave": "w2",
     "title": "Egress test: rent a droplet for an hour, fetch robots + one product page from all 21 sources; rian approves the spend [Run 10 Sep from the real droplet (137.184.166.194) against a same-day mosiah control: 23 of 23 hosts identical (22 pages, Dubai refused on both). Text-fetch only; a bounded rendered run is next. .logs/runs/egress-2026-09-10.md]",
     "status": "done",
     "paid": true,
     "due": "2026-09-07",
     "rian": true
    },
    {
     "id": "E3",
     "wave": "w2",
     "title": "Sizing and real monthly cost to rian; rian decides the hosting figure to tell Adam [Two figures given to rian in chat 10 Sep from the brief's inputs: DFP alone without collectors 2 GB at $12; DFP with the browser sidecar, and as the shared live box, 4 GB at $24 (start here; resize CPU and RAM only). The $50 hold stands (rian, 10 Sep).]",
     "status": "done",
     "paid": true,
     "due": "2026-09-08",
     "rian": true
    },
    {
     "id": "E4",
     "wave": "w2",
     "title": "Provision the DigitalOcean droplet (rian's account and payment), Postgres, firewall [bwlive (137.184.166.194, Tailscale 100.106.255.15) bootstrapped 10 Sep by deploy/bootstrap-droplet.sh: deploy user, sshd keys-only + AllowUsers from the tailnet and mosiah, root off, unattended upgrades, Docker 29, 2 GB swap. Open: rian confirms the DO cloud firewall is attached; the sidecar fence (deploy/sidecar-firewall.sh) awaits his approval.]",
     "status": "done",
     "paid": true,
     "due": "2026-09-08",
     "rian": true
    },
    {
     "id": "E4b",
     "wave": "w2",
     "title": "Production deploy path from this repo; first deploy to the droplet; PUBLIC_BASE_URL set [deploy/production.sh; first deploy 10 Sep 23:51 UTC: seeded from the staging dump, migration head b5c6d7e8f9a0, /api/health 0.35.0, 13,307 products / 37,062 observations / 3 accounts; owner-check ok; PUBLIC_BASE_URL set; sidecar fence proven from inside the browser container.]",
     "status": "done",
     "paid": true,
     "due": "2026-09-09"
    },
    {
     "id": "E4c",
     "wave": "w2",
     "title": "dutyfreeprofessor.com pointed at production with certificates; Cloudflare zone checklist (bot fight mode, AI-bot blocking, managed robots off; cache bypass for /api and /owner) [dutyfreeprofessor.com on the droplet 11 Sep 00:2x UTC via the proxied apex A record (WP Engine records deleted); Cloudflare Origin CA certificate on Caddy (Let's Encrypt's remote validators could not reach the zone, 15 attempts); rian set Full (strict), bots/AI/managed-robots off, bypass rule; verified from three outside vantage points.]",
     "status": "done",
     "paid": true,
     "due": "2026-09-10",
     "rian": true
    },
    {
     "id": "E5",
     "wave": "w2",
     "title": "Host cron: audit nightly, verify after collect and weekly, nightly pg_dump on production [bwlive crontab: pg_dump 03:15 UTC (14 days) pulled to mosiah backups/production/ at 04:15; audit 04:00 UTC nightly; verify --n 200 --mode weekly Sundays 05:00 UTC. verify --n 20 after a collect stays a hand step (RUNBOOK).]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "E6",
     "wave": "w3",
     "title": "Monitoring and alerts to rian; RUNBOOK deploy / rollback / restore; one rehearsed restore [RUNBOOK 'Production' section; DO alert policies set by rian (memory/CPU/disk 80%); restore rehearsed 11 Sep on the droplet into a scratch database: 11 seconds for the 9.6 MB dump, counts match. External uptime check not yet wired (issue filed).]",
     "status": "done",
     "paid": true,
     "due": "2026-09-15"
    },
    {
     "id": "E7",
     "wave": "w4",
     "title": "Go-live checklist on production: every internal route closed, no-store headers, sitemap and robots correct, egress from the new IP re-checked [First pass 11 Sep through the domain: all 29 routes give the sign-in door except /login /welcome /forgot; every API read 401/302 with no-store; robots Disallow all and sitemap 404 (correct while members-only); egress re-checked from the production address (23/23 + rendered). The public flip is its own go-live step: do-flip-production-to-public on the running list.]",
     "status": "done",
     "paid": true,
     "due": "2026-09-16",
     "rian": true
    }
   ]
  },
  {
   "key": "F",
   "title": "F \u00b7 Awards",
   "session": "/stream-f",
   "tasks": [
    {
     "id": "F1",
     "wave": "w3",
     "title": "Selection strategy document for Adam [published as /discuss decision card 5 (item 40) with the full text in its thread; kept at notes/awards-strategy-for-adam-2026-09-05.md; ask recorded as do-adam-awards-strategy, gates F2]",
     "status": "done",
     "paid": true,
     "due": "2026-09-14"
    },
    {
     "id": "F2",
     "wave": "w3",
     "title": "Picker (recency, level, airport affinity); --rebuild upserts by natural key; pin via overrides [picker + rebuild by natural key + 78 tests landed (e2d50a3), rehearsed on a throwaway DB copy; wiring into catalog_queries is issue-wire-award-picker (B); the pin waits for the overrides table (decide-overrides-before-r): it plugs in at catalog_queries.get_product/_top_awards via award_picker.pin_from_override, and the stale-pin reverification in awards_import.import_awards after reconcile_awards]",
     "status": "blocked",
     "paid": true,
     "due": "2026-09-15"
    }
   ]
  },
  {
   "key": "R",
   "title": "R \u00b7 Accounts and owner area",
   "session": "/stream-r",
   "tasks": [
    {
     "id": "O1",
     "wave": "w2",
     "title": "Default-deny auth middleware + route-inventory test (closes nine open write routes) [delivered as N1 (commit d5f89b0): default-deny middleware, mutations.py as the allowlist, owner sign-in stopgap; R's accounts replace the credential]",
     "status": "done",
     "paid": false,
     "due": "2026-09-10"
    },
    {
     "id": "L1",
     "wave": "w2",
     "title": "Vendor the BW kit and React pack byte-identical with a manifest and vendor-check in check.sh; argon2 dependency; the X-Auth-User grep gate [27 files vendored, manifest + vendor-check in check.sh, tsc -b green under the reference; hand edit proven to fail]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "O2",
     "wave": "w2",
     "title": "Migration #4 and the models: accounts columns, account_credentials, levels, members and grants, sessions, account_tokens, audit_log, overrides, the seven who-columns; backfills accounts, levels, authors, overrides; cli_accounts.py; rehearsed on dfp-devdb up and down twice [b5c6d7e8f9a0 rehearsed up/backfills x2/down/up on dfp_r; 7 tests]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "L3",
     "wave": "w2",
     "title": "Identity: argon2id passwords, DB sessions and the loader middleware, hashed single-use tokens carried in the link's fragment, the local directory adapter, the mail seam, the audit sink (remove maps to disable), /api/auth/*, the silent per-account lock and the address throttle [sessions, directory, mail seam, audit sink, /api/auth/*, CLI; 41 tests incl. identical refusal sequence and lockout on a fake clock]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "L5",
     "wave": "w2",
     "title": "Kit wiring (before L4): the mapped store, six permissions registered, seed levels, View As policy with owner-only act mode, build_router at /api/bw, DFP's own /api/bw/me with the capabilities block, /api/whoami, /api/ops/impersonations [mapped store, sentinel owner, can(), View As policy, /api/bw mounted, DFP /api/bw/me; 13 tests]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "L4",
     "wave": "w2",
     "title": "Access policy (after L5): services/access.py over reads, pages and mounts; safe_next; the must-change gate; act_as.write from the middleware; SITE_ACCESS members-only mode; docs off in production; the inventory test rewritten; the stopgap's server half retired [access.py, decide() case table, inventory over reads/pages/mounts/App.tsx, members-mode answers, traversal probes, act_as.write; stopgap retired]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "L6",
     "wave": "w2",
     "title": "Authorship from the session on every write; payload author fields removed; comment edit rules; item:* thread scoping [author from the session in 4 routers; edit rules; item:* scoping; 6 tests]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "L7",
     "wave": "w2",
     "title": "The SPA: login (safeNext), forgot, set-password (token from the hash), account and admin pages; the header account menu; capability guards; name boxes, OwnerGate and api/owner.ts replaced by the capabilities [six pages, account menu, capability guards, name boxes and OwnerGate gone; tsc and build green]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "L8",
     "wave": "w2",
     "title": "Tests: the kit's conformance pack vendored under tests/kit with a DFP conftest; DFP's own suites for policy, sessions, passwords, tokens, lockout, members mode, authorship [45 conformance tests byte-identical + DFP suites; 1016 tests in 7 s]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "L9",
     "wave": "w2",
     "title": "Docs: main/docs/ACCOUNTS.md, ARCHITECTURE, CLIENT-SURFACES, RUNBOOK (Accounts, rollout, transfer of ownership), DATA-MODEL, agents.md pointer, .app.env.example, CHANGELOG, KICKOFFS, the kit upstream proposals [ACCOUNTS.md, RUNBOOK Accounts, CHANGELOG one deploy, upstream proposals; docs-check strict pending final]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "L10",
     "wave": "w2",
     "title": "End-to-end rehearsal on a local server against dfp_r (the curl list and the browser round trip); the handoff with rian's ordered steps; green and ready [27 curls + 23-step scripted round trip green on dfp_r; the browser step is rian's on the public host]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "L11",
     "wave": "w2",
     "title": "If time remains: the Resend mail provider behind MAIL_PROVIDER=resend, inert until its lines exist [ResendMailer, stdlib, inert until MAIL_PROVIDER/RESEND_API_KEY/MAIL_FROM; 4 tests]",
     "status": "done",
     "paid": false,
     "due": "2026-09-12"
    },
    {
     "id": "T1",
     "wave": "buf",
     "title": "R2: migration #6 (threads, comment columns, notifications, email_sends), backfill threads, services/discussion.py ported from the Interaction Standard's reference",
     "status": "todo",
     "paid": false,
     "due": "2026-09-23"
    },
    {
     "id": "T2",
     "wave": "buf",
     "title": "R2: the caddie-ui pack stamped byte-identical; Composer, ThreadCard, Body, Lightbox inline on /discuss; author from the session; own edit, curate any; soft delete",
     "status": "todo",
     "paid": false,
     "due": "2026-09-23"
    },
    {
     "id": "T3",
     "wave": "buf",
     "title": "R2: /structure, /quote, /todo and /plan on the same components with one subjects read per page",
     "status": "todo",
     "paid": false,
     "due": "2026-09-24"
    },
    {
     "id": "T4",
     "wave": "buf",
     "title": "R2: notifications (mention, reply, decision), the inbox routes, the Bell in the header",
     "status": "todo",
     "paid": false,
     "due": "2026-09-24"
    },
    {
     "id": "T5",
     "wave": "buf",
     "title": "R2: attachments in comments through the uploads seam under /api/attachments",
     "status": "todo",
     "paid": false,
     "due": "2026-09-25"
    },
    {
     "id": "T6",
     "wave": "buf",
     "title": "R2: mail: the Resend provider, an app-only env file (security review), email_sends caps, invites and resets by email, the notify digest command",
     "status": "todo",
     "paid": false,
     "due": "2026-09-25"
    },
    {
     "id": "V1",
     "wave": "buf",
     "title": "R3: the /api/ops/* reads (sources, runs, rejections, verify, audit, merges, freshness, impersonations) gated by sources.manage",
     "status": "todo",
     "paid": false,
     "due": "2026-09-25"
    },
    {
     "id": "V2",
     "wave": "buf",
     "title": "R3: the collection page; the kill switch toggle with an actor in the audit log; verify-clear from the session; the sources enable/disable CLI",
     "status": "todo",
     "paid": false,
     "due": "2026-09-26"
    },
    {
     "id": "V3",
     "wave": "buf",
     "title": "R3: prune commands (sessions, tokens, audit-log) and their cron lines on the machine E5 picks",
     "status": "todo",
     "paid": false,
     "due": "2026-09-26"
    },
    {
     "id": "V4",
     "wave": "buf",
     "title": "R3, gated: the scheduler as a watched trial behind FEATURE_SCHEDULER, only after decide-where-collectors-run and the hand-run decide are answered",
     "status": "todo",
     "paid": false,
     "due": "2026-09-26"
    },
    {
     "id": "O3",
     "wave": "buf",
     "title": "Five owner surfaces (issues, summaries, posture, inbox, drafts) [superseded 10 Sep by the accounts plan: the owner surfaces are R3's ops page (V1, V2) and R2's inbox]",
     "status": "deferred",
     "paid": false,
     "due": "2026-09-25"
    },
    {
     "id": "T7",
     "wave": "buf",
     "title": "R2: a staging refresh preserves discussion_comments and every table Adam writes; the procedure in the runbook, rehearsed on dfp-devdb",
     "status": "todo",
     "paid": false,
     "due": "2026-09-14"
    },
    {
     "id": "T8",
     "wave": "buf",
     "title": "R2: the pages collapsed: the proposal written into the brief first, every subject key kept, every old address redirecting",
     "status": "todo",
     "paid": false,
     "due": "2026-09-15"
    },
    {
     "id": "T9",
     "wave": "buf",
     "title": "R2: issues that resolve and archive from the page, the archived set reachable; the write path R3's clear will share",
     "status": "todo",
     "paid": false,
     "due": "2026-09-15"
    },
    {
     "id": "T10",
     "wave": "buf",
     "title": "R2: a comment mentioning Claude on a rian-only page is a subject a session reads on its next run; a test pins that a plan thread never reaches a client",
     "status": "todo",
     "paid": false,
     "due": "2026-09-15"
    }
   ]
  },
  {
   "key": "DOCS",
   "title": "Docs",
   "session": "/stream-docs",
   "tasks": [
    {
     "id": "X1",
     "wave": "w1",
     "title": "Hygiene: old-name headers, brief rewrite with NOW block, dead files, poc-dfp delivered [brief 40 lines with NOW block; 5 headers retitled; nginx.conf + deploy-failed.log gone; guardrails moved to main/docs/COLLECTORS.md; proposal bannered]",
     "status": "done",
     "paid": false,
     "due": "2026-09-05"
    },
    {
     "id": "X2",
     "wave": "w1",
     "title": "agents.md to \u2264140 lines; main/docs shells (COLLECTORS, RUNBOOK, QUALITY, ARCHITECTURE, DATA-MODEL) [agents.md 129 lines; seven main/docs shells each opening Sources of truth; check.sh green]",
     "status": "done",
     "paid": false,
     "due": "2026-09-05"
    },
    {
     "id": "X3",
     "wave": "w1",
     "title": "docmap.py + docs-check.sh FAIL gates; project /checkpoint command [docmap.py 3 blocks; docs-check.sh FAIL+WARN tiers wired into check.sh; project /checkpoint loads; gate found version.py lagging CHANGELOG]",
     "status": "done",
     "paid": false,
     "due": "2026-09-05"
    },
    {
     "id": "X4",
     "wave": "buf",
     "title": "WARN gates, weekly doc sweep, CLIENT-SURFACES/SEO docs",
     "status": "deferred",
     "paid": false,
     "due": "2026-09-23"
    }
   ]
  },
  {
   "key": "OWN",
   "title": "Rian",
   "session": null,
   "tasks": [
    {
     "id": "P1",
     "wave": "w1",
     "title": "Decisions \u00a710 #1 and #2 (scope vs capacity; the Fri 11 milestone) [decided 5 Sep: everything stays, unpaid ranked last, pulled forward Fri 11 if green; Fri 11 = option (a)]",
     "status": "done",
     "paid": false,
     "due": "2026-09-05",
     "rian": true
    },
    {
     "id": "P2",
     "wave": "w1",
     "title": "Send Mark the /structure page after B3; comments by Thu 10 [sent 5 Sep with the /todo page; comments by Thu 10]",
     "status": "done",
     "paid": false,
     "due": "2026-09-06",
     "rian": true
    },
    {
     "id": "P3",
     "wave": "w2",
     "title": "Reply pass to Adam's sixteen threads (plan \u00a710 #15), Mon 7 [reply pass done 5 Sep; some threads needed no answer]",
     "status": "done",
     "paid": false,
     "due": "2026-09-07",
     "rian": true
    },
    {
     "id": "I1",
     "wave": "w4",
     "title": "Wave 4 integration: full test run, docs-check strict, cross-device, accessibility, performance",
     "status": "todo",
     "paid": true,
     "due": "2026-09-17"
    },
    {
     "id": "P4",
     "wave": "w2",
     "title": "Deploy checkpoints midday/evening; dump before any migration",
     "status": "todo",
     "paid": false,
     "due": "2026-09-18",
     "rian": true
    },
    {
     "id": "P6",
     "wave": "w4",
     "title": "Delivery walkthrough with Adam",
     "status": "todo",
     "paid": false,
     "due": "2026-09-18",
     "rian": true
    },
    {
     "id": "P5",
     "wave": "buf",
     "title": "Cannes asks: Dubai, Avolta pagination/sitemaps, Shilla",
     "status": "todo",
     "paid": false,
     "due": "2026-09-26",
     "rian": true
    }
   ]
  },
  {
   "key": "S",
   "title": "S \u00b7 Structure review",
   "session": "/stream-s",
   "tasks": [
    {
     "id": "S1",
     "wave": "w2",
     "title": "Airport URL leads with the airport's common name (JFK code-first); every earlier shape 301s [e119380; curl-verified on dfp_s: new path 200, six old shapes 301 keeping the query, jfk-new-york 200, hidden airport 404]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "S2",
     "wave": "w2",
     "title": "Review outcomes in code: the Perfume category; a brand page needs three products at two airports; the price of record leads the page and is the only price in the markup [d4567e6 Perfume (+backfill perfume, rehearsed 2,219 rows on dfp_s), fe5ffa9 brand floor (44 pages on dfp_s at four airports), next commit price of record; 859 tests, check.sh green]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "S3",
     "wave": "w2",
     "title": "/structure rebuilt around the review outcome; /todo airport ask reworded; built rows flipped to live [commit on /structure and /todo; check.sh green; seed applied live, row 9 kept id, status and comment 148]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    },
    {
     "id": "S4",
     "wave": "w2",
     "title": "Drafts for rian: the nineteen airports, answers to the open questions, the frame for Adam's two documents [notes/review-drafts-2026-09-10.md: the nineteen with names from the database, answers to the four open questions with the build assumptions, the frame for Adam's two documents (both answer with a sign-in page; not read), the todo:9 reply]",
     "status": "done",
     "paid": false,
     "due": "2026-09-11"
    }
   ]
  },
  {
   "key": "N",
   "title": "N \u00b7 Night of 10 Sep",
   "session": "/stream-n",
   "tasks": [
    {
     "id": "N1",
     "wave": "w2",
     "title": "O1: default-deny auth middleware, the mutating-route inventory as its allowlist, an owner login for the routes behind it [commit; 883 tests incl. 26 policy tests; curl-verified on dfp_n: owner routes 403 without cookie, admitted with it, Origin mismatch refused, public routes unchanged]",
     "status": "done",
     "paid": false,
     "due": "2026-09-10"
    },
    {
     "id": "N2",
     "wave": "w2",
     "title": "Every document current: main/docs against their sources, tier-1 files, stream briefs, plan statuses; the issue log complete [commit: seven docs checked against sources and corrected, agents.md pointers, stream briefs stamped, plan statuses corrected, handoff trimmed, fourteen items filed and one resolved; docs-check --strict 0 fail]",
     "status": "done",
     "paid": false,
     "due": "2026-09-10"
    },
    {
     "id": "N3",
     "wave": "w3",
     "title": "Awards: a shorter competition entry no longer lands on the longer retail variant; medal artwork gaps [d50a352 matcher veto + apostrophe fix (rehearsed on dfp_n, every changed row read; review file); artwork commit with test_medal_artwork.py]",
     "status": "done",
     "paid": false,
     "due": "2026-09-10"
    },
    {
     "id": "N4",
     "wave": "w2",
     "title": "Home page: the hero's airport picker no longer shifts the card when its list arrives [commit; Lighthouse mobile home CLS 0.0166 to 0.0007, perf 97, two runs each side]",
     "status": "done",
     "paid": false,
     "due": "2026-09-10"
    },
    {
     "id": "N5",
     "wave": "w4",
     "title": "Review pass: every route against a restored dump, house style in shopper-facing text, doc gates strict, obvious fixes landed, the rest filed [159 routes, sitemap entries and API reads all 200 on dfp_n, no tracebacks, no console errors; fifteen em dashes reworded (c3e5f09) with test_house_style.py; Lighthouse mobile home 97 / product 93 / airport 98 / brand 97, a11y 100, CLS 0; favicon and the rest filed]",
     "status": "done",
     "paid": false,
     "due": "2026-09-10"
    }
   ]
  },
  {
   "key": "M",
   "title": "M \u00b7 Matching and merging",
   "session": "/stream-m",
   "tasks": [
    {
     "id": "M1",
     "wave": "w3",
     "title": "Listings view on /collectors: the tile as collected beside what we made of it",
     "status": "todo",
     "paid": false,
     "due": "2026-09-13"
    },
    {
     "id": "M2",
     "wave": "w3",
     "title": "The line above the product and the variation vocabulary (migration + backfills, rehearsed)",
     "status": "todo",
     "paid": false,
     "due": "2026-09-13"
    },
    {
     "id": "M3",
     "wave": "w3",
     "title": "match_key v3: brand prefix and concentration stripped; the 100 ml 1 Million EDT becomes one product at five airports",
     "status": "todo",
     "paid": false,
     "due": "2026-09-14"
    },
    {
     "id": "M4",
     "wave": "w3",
     "title": "Aliases at every level with a preferred name; Paco Rabanne \u2192 Rabanne as rian's first confirmed alias",
     "status": "todo",
     "paid": false,
     "due": "2026-09-14"
    },
    {
     "id": "M5",
     "wave": "w3",
     "title": "Suggestions, rules first: brand, line and product candidates with a readable reason and a score",
     "status": "todo",
     "paid": false,
     "due": "2026-09-15"
    },
    {
     "id": "M6",
     "wave": "w3",
     "title": "The merge session on /collectors: keyboard confirm / reject / skip, preferred name, decided_by recorded",
     "status": "todo",
     "paid": false,
     "due": "2026-09-15"
    },
    {
     "id": "M7",
     "wave": "w3",
     "title": "Inheritance rules for barcode and image, written and tested; never invented",
     "status": "todo",
     "paid": false,
     "due": "2026-09-15"
    }
   ]
  },
  {
   "key": "G",
   "title": "G \u00b7 The complete airport page",
   "session": "/stream-g",
   "tasks": [
    {
     "id": "G1",
     "wave": "w3",
     "title": "Opening hours collected where the airport operator's robots allow: a robots verdict per operator domain, one collector per operator platform, the airport_hours table (migration, rehearsed) [robots read per operator domain (18 domains, verdicts in .logs/runs/hours-robots-2026-09-11.md); collectors for Heathrow, Dublin and Toronto Pearson read the live pages into dfp_g rows 1-3; migration c7d8e9f0a1b2 rehearsed up/down/up; 32 hours tests; 16 airports to hand population]",
     "status": "done",
     "paid": false,
     "due": "2026-09-14"
    },
    {
     "id": "G2",
     "wave": "w3",
     "title": "Hours by hand: app.cli hours set, the same shape as a collected set, with the date and who entered it; a human value is never overwritten by a collector [hours set writes a dated hand row named to the account (proved on dfp_g: ATH row 4); the reader shows hand over collected and stores a later collect unshown; backfill hours_seed idempotent (seeds zero today, no guide carries an hours constant); guide_for(iata, db) reads the table; 9 store tests on SQLite]",
     "status": "done",
     "paid": false,
     "due": "2026-09-14"
    },
    {
     "id": "G3",
     "wave": "w3",
     "title": "Provenance per airport: collected or hand-entered and the date, nothing where neither exists; the read and the component, the shape written down for the review area [GET /api/airports/{iata}/hours (public-when-open, in open_routes.json) and guide.hours_provenance; the facts panel prints 'collected 11 Sep 2026' beside the hours on the server and in AirportHours.tsx, the placeholder view the full line; proved on dfp_g for LHR (collected) and ATH (hand); COLLECTORS.md Opening hours section; issues filed for M and R3; 1123 tests, check.sh green]",
     "status": "done",
     "paid": false,
     "due": "2026-09-15"
    },
    {
     "id": "G4",
     "wave": "w3",
     "title": "Featured at this airport, by family [services/airport_featured.py runs featured.pick_featured per family over the airport's own rows; AirportDetail.featured, one row per family in seo.airport_body and AirportFeatured.tsx, absent where nothing compares; proved on dfp_g (LHR, DUB, ZRH rows by family); 6 picker tests on seeded rows plus the body pins; check.sh green]",
     "status": "done",
     "paid": false,
     "due": "2026-09-15"
    },
    {
     "id": "G5",
     "wave": "w3",
     "title": "Shopping-feature filters on the airport page (award winners, travel exclusives, comparable only), pointing back at the clean address [the three chips (Comparable, Award winners, Travel exclusives) on the airport page's full list as ?multi_only|awarded_only|exclusives_only=true, the names /api/products takes, kept across category chips, sort and pages, dropped on the other shelves; canonical stays the clean page (pinned in test_seo_airport and test_site_routes); urls.py and urls.ts move together (the mirror test reads both); GET /api/airports/{iata} takes them; check.sh green]",
     "status": "done",
     "paid": false,
     "due": "2026-09-15"
    },
    {
     "id": "G6",
     "wave": "w3",
     "title": "A comparison preset: the comparison tool opened on this airport [Compare from here on the airport hero (server body and SPA) opens /savings?from=<IATA>; SavingsPage holds the departure as view state read once on mount, leads the comparison with it beside the saved airports and never writes the saved set; urls.py savings_from_path mirrored by urls.ts savingsFromPath/savingsDeparture (pinned); check.sh green]",
     "status": "done",
     "paid": false,
     "due": "2026-09-16"
    },
    {
     "id": "G7",
     "wave": "w3",
     "title": "Category and category-at-airport pages at the structure proposal's thresholds; beauty switches on as its coverage qualifies [category-at-airport pages at /airports/<airport>/<category-slug>, built only where the pair holds 15+ published products (coverage.CATEGORY_AT_AIRPORT_MIN_PRODUCTS, read at request time; a decide records the number): server body and head, the SPA page, the API, the sitemap, the airport page's rail of links; 96 pages across 17 airports on the 11 Sep copy, LHR whisky proved; the site-wide category page is not built (structure.ts names /alcohol/<slug>, not /categories/; a decide names it); check.sh green]",
     "status": "done",
     "paid": false,
     "due": "2026-09-16"
    }
   ]
  },
  {
   "key": "W",
   "title": "W \u00b7 Go-live readiness",
   "session": "/stream-w",
   "tasks": [
    {
     "id": "W1",
     "wave": "w4",
     "title": "Privacy, terms and contact pages served by rule from public/, linked from the footer; the wording marked draft until Adam approves it",
     "status": "todo",
     "paid": false,
     "due": "2026-09-16"
    },
    {
     "id": "W2",
     "wave": "w4",
     "title": "The lazy chunks gated behind the session while members-only; the main bundle and the sign-in chunk stay public; a test pins both",
     "status": "todo",
     "paid": false,
     "due": "2026-09-16"
    },
    {
     "id": "W3",
     "wave": "w4",
     "title": "An app-only env file in compose so the database container reads no app secret: security review, the change staged and audited, the droplet twin as a checklist for rian",
     "status": "todo",
     "paid": false,
     "due": "2026-09-17"
    },
    {
     "id": "W4",
     "wave": "w4",
     "title": "The unauthenticated sweep as a script: every App.tsx route and its /api/* reads curled without a session, the internal set proved closed; runs again on the day",
     "status": "todo",
     "paid": false,
     "due": "2026-09-17"
    },
    {
     "id": "W5",
     "wave": "w4",
     "title": "The production edge checklist with the exact commands: trusted proxies on the droplet, cache bypass for the auth paths, Bot Fight Mode off; rian runs it",
     "status": "todo",
     "paid": false,
     "due": "2026-09-17"
    },
    {
     "id": "W6",
     "wave": "w4",
     "title": "The Extime-is-Paris correction in copy with no address change; the IndexNow mint written as rian's one command block, never run here",
     "status": "todo",
     "paid": false,
     "due": "2026-09-18"
    }
   ]
  }
 ],
 "quote": [
  {
   "key": "core-environment",
   "name": "Live production environment",
   "tasks": [
    "E1",
    "E3",
    "E4",
    "E4b",
    "E4c",
    "E5",
    "E6",
    "E7"
   ]
  },
  {
   "key": "core-airports",
   "name": "Sixteen airports live (nineteen at launch)",
   "tasks": [
    "A10",
    "A6",
    "A13"
   ]
  },
  {
   "key": "core-quality",
   "name": "Quality, performance and soft launch",
   "tasks": [
    "Q1",
    "Q2",
    "Q3",
    "Q4",
    "I1",
    "P6"
   ]
  },
  {
   "key": "core-crawlable",
   "name": "Emitting HTML that AI can read",
   "tasks": [
    "B1",
    "B2"
   ]
  },
  {
   "key": "core-brand",
   "name": "Brand alignment",
   "tasks": [
    "C1",
    "C5"
   ]
  },
  {
   "key": "structure-proposal",
   "name": "Site structure proposal",
   "tasks": [
    "B3",
    "P2"
   ]
  },
  {
   "key": "airport-pages",
   "name": "Airport pages",
   "tasks": [
    "B4"
   ]
  },
  {
   "key": "brand-pages",
   "name": "Brand pages and browsable brands",
   "tasks": [
    "A8",
    "A9",
    "B5"
   ]
  },
  {
   "key": "mega-menu",
   "name": "Mega menu",
   "tasks": [
    "C2"
   ]
  },
  {
   "key": "articles",
   "name": "Adam's articles published",
   "tasks": [
    "D1",
    "D2"
   ]
  },
  {
   "key": "prominence",
   "name": "Featured savings and exclusives given prominence",
   "tasks": [
    "C3"
   ]
  },
  {
   "key": "categories",
   "name": "Two further product types, twenty each",
   "tasks": [
    "A7",
    "A11"
   ]
  },
  {
   "key": "awards",
   "name": "Awards selection strategy",
   "tasks": [
    "F1",
    "F2"
   ]
  },
  {
   "key": "email-capture",
   "name": "Email capture",
   "tasks": [
    "D3"
   ]
  },
  {
   "key": "sponsor-slots",
   "name": "Sponsor banner positions",
   "tasks": [
    "C5",
    "D4"
   ]
  },
  {
   "key": "new-collectors",
   "name": "Three new data collectors",
   "tasks": [
    "A0",
    "R1",
    "R2",
    "R3",
    "R4"
   ]
  },
  {
   "key": "merge-session",
   "name": "One page per real product: the merge session (rian's decision, 12 Sep; unpaid)",
   "tasks": [
    "M1",
    "M2",
    "M3",
    "M4",
    "M5",
    "M6",
    "M7"
   ]
  }
 ]
}
