# The away plan
<!-- Role: the plan for the days rian is away. It carries the decisions taken in his absence,
     the lanes that can run at the same time, and the instruction for the session that turns
     them into runnable briefs. Retire it when rian clears the review item on the running list. -->

Written by the planning session that proposed the lanes, from rian's ask: get streams running in
parallel while he is away, and decide the open questions myself rather than leave them waiting.

**Nothing here is rian's decision.** Every call below is a working assumption a session proceeds
under, recorded so he can overturn it in one sentence when he is back. The cost of overturning
each one is stated.

---

## 1. For the session that finalises this (read this section first)

Your job is to turn this plan into runnable streams, then stop. You do not build features, you do
not run a collection, and you do not start the streams.

1. `pwd` must be `/srv/apps/dutyfreeprofessor`. If it is not, stop.
2. This plan arrives on a branch. If `git log --oneline -1` does not show the `Away:` commit,
   merge it first: `git merge claude/stream-ideas-parallel-57686d` (a fast-forward).
3. Read, in order: `.logs/planning/streams/OVERNIGHT-RULES.md` (binding), this file in full,
   `.logs/planning/streams/KICKOFFS.md`, `agents.md`, `brief.md`, build plan
   `.logs/planning/build-plan-2026-09.md` sections 2, 4, 7, 8, 9 and 10, then
   `python3 main/scripts/items.py list` and the top three entries of `.logs/handoff.md`.
4. **Write the missing briefs and commands.** Section 4 names every lane and says which already
   have a brief. For each one that does not, write `.logs/planning/streams/<X>.md` and
   `.claude/commands/stream-<x>.md`. Copy the shape of `M-merging.md` and `stream-m.md`, which are
   the newest and the model to follow: a dated status line, a goal, numbered tasks each with an id,
   then Owns, Must not touch, Rules; and in the command, the read order, the numbered done
   conditions, and the goal line rian pastes.
5. **Register every task id** in `import/progress.json` through `main/scripts/plan-set.py`, never
   by hand, so `/plan` shows each lane before it starts. Section 4 gives each lane its id prefix.
6. **Record the decisions in section 3 on the running list as assumptions, not as answers.** Only
   rian resolves a decide. For an item that already exists, leave it open: the brief that depends
   on it states the assumption it proceeds under, in its own words, with what gets redone if rian
   chooses otherwise. For the two new ones (opening hours, and the lazy chunks) add a decide item
   with `--assumption` set to the call in section 3.
7. **Add one item for rian**: `--kind do --owner rian --title "Review the decisions taken while you
   were away" --detail` pointing at this file, `--weight costly`.
8. **Prove the lanes cannot collide.** For the three wave-one lanes, print every brief's Owns and
   Must not touch and show that no path appears in two Owns columns. If two do, the file belongs to
   one lane and the other raises an issue for it instead. Report the result in your handoff.
9. Run `main/check.sh`, commit with the prefix `Away:`, prepend a handoff entry, run `/checkpoint`,
   print the paste block from section 5, and **stop**. Do not start a stream.

---

## 2. Standing rules while rian is away

These sit on top of `OVERNIGHT-RULES.md`, which still binds every session.

- **Staging only.** Rian's instruction: crawl on staging, push live after. No production deploy, no
  production migration, nothing run on `bwlive`, for the whole period.
- **Production is untouchable regardless** until `.logs/runs/window-2026-09-11-night-refresh.md`
  says the window is finished.
- **No network collection against a host that has already refused us.** The Singapore retry waits
  for rian (section 3). A single robots.txt read per host to test code is still fine.
- **Opening hours are a new source class.** Every airport operator domain gets its own robots read
  before it is added, and permission never transfers from one to another, exactly as retailer
  domains work today.
- **Three stream sessions at once, no more** (Decision 13), in the one working tree at
  `/srv/apps/dutyfreeprofessor`.
- **Nothing irreversible.** No data is deleted, no address changes that would need a redirect after
  launch, no visible brand change. Anything in that class is left for rian and said so out loud.

---

## 3. The decisions taken, and what each costs to overturn

### The three that were holding work up

**Singapore's 403 on the 19th page: hold, no retry while rian is away.**
The recommended option is one slower retry at a thirty-second pace, stopping for good on a second
refusal. That is still the right call, but the outcome is consequential and one-way: a second 403
ends Singapore for good, and it should not be spent by an unattended session. The seventeen prices
already stored are real and dated. This blocks the Singapore collector only, which is in no lane.
*To overturn: say retry, and a session runs it in an announced window.*

**Which page types ship on 18 Sep: all six where the coverage thresholds are met, with beauty
switching on as its coverage crosses the bar.**
The thresholds in the structure proposal already stop anything thin from being built, which is the
whole reason they exist, and Mark's comments are in. This unblocks the category and
category-at-airport pages inside the airport lane.
*To overturn: category pages move to the buffer week and the lane drops two tasks. Cheap before
they are built, a redirect afterwards.*

**What the review area needs next: the stated order stands.**
Act on a duplicate pair from the page, clear a publication block from the page, a per-airport view,
saved views, then catalogue counts over time. The merge lane delivers the first as its merge
session; the oversight lane delivers the second, fourth and fifth; the per-airport view is where
the opening-hours provenance lands, so the airport lane and the oversight lane meet there.
*To overturn: reorder the tasks. Free until they are started.*

### Rian's own decision of the 11th, recorded

**Opening hours: collect where the airport operator's robots allow, leave the airport for hand
population where they do not, and show which on the collection page.**
Two riders that follow from the posture rather than from choice. Each operator domain is its own
host with its own robots file, checked before it is added. And the page shows, per airport, whether
the hours were collected or hand-entered and the date they were, so a stale hand-entered set is
visible rather than silent.

### The three that were already overtaken, now ratified

**The four standing rules: three as stated, one exception.**
Blocked shops stay public on their last-seen prices with the date shown and a flag; a robots file
that answers 404 means open while 401 or 403 means refused; host cron for the audit and verify runs
is not automated collection. The exception is the fourth: the pages-2-and-later observations at the
four Avolta stores that forbid query pagination, collected against their robots before the fix.
They are **marked unpublished, not purged**, so nothing built on them can reach a page, and the
purge itself waits for rian. Deleting stored observations is not something an unattended session
should do, and quarantine buys the same clean posture today.
*To overturn: purge them, or republish them. Both are one command.*

**The owner sign-in stopgap: ratified as built.** R1's accounts replaced the credential, so the
question is now historical.

**The login wall comes off with E7 on go-live day.** Mark's preference is to index as late as
possible, and go-live day is that date. The flip is one environment line and reversible in minutes.
*To overturn: name a later date; nothing is built on it either way.*

### The standing assumptions, all kept as they are

Each of these is a one-line change whenever rian wants it, and no lane depends on any of them.

- Shopify shops keep every beauty row rather than only the targeted forty. The rows arrive in the
  same requests, so they cost nothing extra and give Paris its widest comparison set.
- The nineteen airports' web addresses stand as built, with JFK the only code-first one.
- The AI training stance in robots.txt stays no, with search and AI answers yes.
- The dataset page states no licence and offers no download.
- Makeup stays in the category vocabulary.
- The drafted consent sentence ships on the subscribe form.
- Sizes show one unit on the page, with the shop's own wording kept in the product name as the
  record.
- Souvenir goods in the Paris catalogue keep being collected and stay uncategorised, so no page
  shows them.

### The two I changed, and the one I deliberately did not take

**The lazy chunks get gated behind the session while the site is members-only.**
The hashed build assets carry the quote, structure and review pages' static copy, so a determined
anonymous visitor could read Adam's and Mark's pre-launch material. It was called an afternoon in
the access policy plus a test. It goes to the readiness lane.
*To overturn: leave them public. Reverting is deleting the rule.*

**Mark's note against dark backgrounds: not taken.**
Lightening the header is a visible brand change, and a visible brand change made while rian is away
is exactly the kind of thing he should see himself. The header and hero stay navy. It is the one
open decide I am leaving untouched on purpose.

---

## 4. The lanes

Three run at once. Wave two starts as each wave-one lane finishes, in the order given. The merge
lane and the collector lane share the identity layer and **must never run at the same time**, which
is why the collector lane is the one that replaces merge.

| Wave | Lane | Command | Brief | Task ids | State |
|---|---|---|---|---|---|
| 1 | R2 discussions and the page consolidation | `/stream-r2` | `R-accounts.md` + plan section 8 | T1 to T6, plus the new consolidation tasks | brief exists, **must be widened** |
| 1 | M standardise and merge | `/stream-m` | `M-merging.md` | M1 to M7 | ready as written |
| 1 | G the complete airport page | `/stream-g` (new) | `G-airport.md` (new) | G1 to G7 | **to write** |
| 2 | R3 collection oversight | `/stream-r3` | `R-accounts.md` + plan section 9 | V1 to V4 | brief exists, V4 stays gated |
| 2 | A collector hardening | `/stream-a` | `A-data.md` | A14 onward | **tasks to add**; never with M |
| 2 | W go-live readiness | `/stream-w` (new) | `W-readiness.md` (new) | W1 to W6 | **to write** |
| filler | Docs X4 | `/stream-docs` | `DOCS.md` | X4 | brief exists |

### R2, widened: discussions and the page consolidation

Plan section 8 already specifies the discussion system in full: threads, mentions parsed
server-side, the bell, decision as an event kind, attachments, mail, and the backfill that keeps
every existing comment. Take it as written, in its slices T1 to T6. Three things rian asked for on
top, which become the tasks after T4.

- **Collapse the pages.** `/discuss`, `/issues`, `/todo`, `/plan`, `/structure` and `/quote` are six
  surfaces onto what is really three things: the running list, the client's material, and the
  build. Propose the smallest set of pages that keeps every subject key working (the keys are
  frozen in plan section 8 and must never be renamed), get the redirects right, and write the
  proposal into the brief before moving a route. No address that Adam or Mark has been given may
  break: every old one redirects.
- **Issues that resolve and archive.** The running list already has the states; the page needs to
  show them, let rian resolve and archive from the page, and keep the archived set reachable. This
  is the same write path as the oversight lane's publication-block clearing, so the two lanes must
  agree on it before either builds it: whichever starts first owns the shared route, the other
  raises an issue for what it needs.
- **A place for rian to talk to Claude.** On the rian-only pages, a comment that mentions Claude is
  a subject a session reads on its next run. The mention directory is one of the five hooks the
  standard already asks DFP to supply, so this is a directory entry and a read, not a new mechanism.
  It must not notify Adam or Mark: the mention directory is scoped by the subject's permission
  already, and a test pins that a plan thread never reaches a client.

**Adam is commenting on staging, not on production.** Every staging refresh from now on preserves
`discussion_comments`, or Adam gets pointed at production instead. Settle it in this lane and write
the answer into the runbook, because a refresh that loses his comments is not recoverable.

### G, the complete airport page

The airport page's own placeholder view already names what is missing, which is the task list.

- **G1 Opening hours, collected where robots allow.** One collector per airport operator platform,
  not per airport, as the collector rule says. Robots read per domain before the domain is added.
  Where the answer is no, the airport is left for hand population and says so.
- **G2 Hours by hand.** A recorded path for a hand-entered set with the date and who entered it,
  the same shape as a collected one, so the page does not care which it got.
- **G3 Provenance on the collection page.** Per airport: collected or hand-entered, the date, and
  nothing at all where neither exists. This lands in the per-airport view of section 3's order, so
  agree the shape with the oversight lane before building it.
- **G4 Featured at this airport**, by family.
- **G5 Shopping-feature filters** on the airport page.
- **G6 A comparison preset** that opens the comparison tool on that airport.
- **G7 Category and category-at-airport pages**, which section 3 unblocks, at the thresholds the
  structure proposal already sets.

### W, go-live readiness

Everything that makes go-live day a flip rather than a project, none of which touches production.

- **W1** The privacy, terms and contact pages, which the running list has been carrying as an open
  issue since the Heathrow guide landed.
- **W2** Gate the lazy chunks behind the session while members-only, per section 3.
- **W3** Split an app-only env file, so the database container stops reading every line of
  `.app.env` before any mail or Google secret lands. A compose change: security review first, then
  `srv-gw security-audit`.
- **W4** The unauthenticated sweep: every route in `App.tsx` and its `/api/*` reads, curled without
  a session, with the internal set proved closed. Written as a script that runs again on the day.
- **W5** The production edge issues recorded against E: trusted proxies on the droplet, the cache
  bypass for the auth paths, Bot Fight Mode. Written as a checklist with the exact commands, run by
  rian when he is back.
- **W6** The Extime-is-Paris correction and the mint of the IndexNow key, both carried as open items.

### A, collector hardening (after M finishes)

The Paco Rabanne and Rabanne brand alias, which the merge lane may already have taken; the six
Avolta stores showing our own breakage rather than a refusal; the four Heinemann-family shops that
worked and then refused, recorded as refusals and never retried; the 700ml against 70cl size
nomenclature; and the shops and terminals within one airport, which the running list says are not
modelled.

---

## 5. Starting them

The finalising session prints this, filled in. Each line is one terminal, from
`/srv/apps/dutyfreeprofessor`. Start the command, let its first turn finish, then paste the goal
line printed at the bottom of that command file.

```
cd /srv/apps/dutyfreeprofessor && claude    # then: /stream-r2
cd /srv/apps/dutyfreeprofessor && claude    # then: /stream-m
cd /srv/apps/dutyfreeprofessor && claude    # then: /stream-g
```

When one finishes, its terminal takes the next wave-two lane in the table's order, except that the
collector lane may only start once the merge lane is done.
