---
description: Stream L: the quantity model and rules v4, decided values that survive rekey, the listed columns, the giant Listings table with the column picker, and the merge desk where rian approves suggested merges in bulk
---

You are running **Stream L** of the Duty Free Professor build, as an autonomous session. Start by
running `pwd`; it must be `/srv/apps/dutyfreeprofessor` (the main tree, as OVERNIGHT-RULES requires;
not a worktree, which has no `backups/` to restore the dump from), or stop.

Read first, in this order: `.logs/planning/streams/OVERNIGHT-RULES.md` (binding), then your brief
`.logs/planning/streams/L-listings.md` (it names what already exists, the exact columns, the key
encoding, the risks in order and the after-deploy commands; build on it), `agents.md`, `brief.md`,
build plan `.logs/planning/build-plan-2026-09.md` sections 2 (tokens, the who-columns rule, migration
order), 4 and 9, `main/docs/DATA-MODEL.md`, `main/docs/RUNBOOK.md` (Migrations, after-deploy backfills,
the dfp-devdb rehearsal), `python3 main/scripts/items.py list --decisions` (Decision 3 on Dubai,
Decision 6 on merges, the 10 Sep decision that collectors run on the droplet, and rian's 14 Sep
three-layer decision are the mandate), the top three entries of `.logs/handoff.md`, and
`app/services/normalize.py`, `app/services/keying.py` and `app/services/merges.py` in full before you
change identity, plus `app/services/collector_view.py` from `collected_tile` to `_listing_rows` before
you move the reader.

Then work the brief's tasks in order, LT1 to LT8, one at a time, each verified and committed and
recorded on `/plan` with `main/scripts/plan-set.py` (`doing` when you start, `done` with a `--note`
that says what proves it, `blocked` with why). Commit prefix `L:`. Delegate mechanical edits (fixture
trimming, the five collectors' quantity reads from a precise spec, the column registry, CSS, docstring
passes) to a smaller model and verify the diff yourself; never delegate the quantity parser's rules,
the key encoding, the override reader, the proposal files, or anything a person reads on the listings
or merge page. **Never run a collector, never deploy, never touch production, never contact a host
that has refused us, never touch the live database beyond read-only SELECTs and one read-only
`pg_dump`.** Rehearse the migration up/down/up and every backfill (`--check`, then twice) on
`dfp-devdb` (127.0.0.1:5433) in `dfp_l`, a fresh copy of
`/srv/apps/dutyfreeprofessor/backups/dfp-nightly-2026-09-14.dump` (the absolute path; `backups/` is
gitignored), in exactly the brief's after-deploy order (quantities, variations, lines, rederive, merges
--check read, merges), and report the numbers it produced, read from that database. Run `alembic heads` immediately before
writing the migration and immediately before committing it; the head must be exactly one, the revision
is `b3c4d5e6f7a8` revising `a2b3c4d5e6f7` (chain onto a newer head if one has appeared), and the pin in
`tests/test_accounts_backfills.py` moves in the same commit. Read `backfill merges --check` before any
real run of it. If the night runs long, cut in the brief's stated order (the proposal files, then the
CSV, then the brands table, then the flavor rule, then the row actions) and say so in the task note; at
turn 120 with LT6 not started, cut the proposal files and the CSV at once;
never cut LT1, LT2, LT3's override reader and tombstone follow, LT4, LT5, the column picker and SQL sorts
in LT6, the batch route in LT7, or LT8. No em dashes, "free" or "cheap" in anything a person reads;
`color`, never `colour`.

The one outcome that matters: when you are done, rian can deploy, run the after-deploy commands in
the order the handoff gives, open `/collectors`, go to Listings, turn every column on, sort and filter
the whole 24k rows with the database doing the work, download the CSV, and start the manual review;
go to Merge, open the Desk, and approve the suggested and proposed brand and line merges in bulk as
his own decisions, with grams and millilitres never confused, each decision recorded against his
account and surviving every later rekey and collection; and read in the handoff which stale shop he
can recrawl (Extime, with the commands drafted) and exactly why the other five are not touched.

## Goal, and exactly when you are done

**Goal:** the quantity parser, the v4 key and variation kinds; one rehearsed migration on a single
head; the decided layer that survives rekeys and re-collection; quantity through the collectors,
ingest and the rehearsed rederive; the listed columns at ingest and by backfill; the giant table with
the column picker, SQL sorts and CSV; the merge desk with batch approve, proposals and the files rian
approves; and the honest recrawl plan, to the verified-and-committed standard.

**You are done when ALL of the following are true**, and not before:
1. Every one of these task ids in `import/progress.json` is `done`, or is `blocked` with a
   note saying why: **LT1, LT2, LT3, LT4, LT5, LT6, LT7, LT8** (never `L1` to `L8`; those are Stream
   R's finished rows). (Check with `python3 -c "import json;d=json.load(open('import/progress.json'));print({t['id']:t['status'] for s in d['streams'] for t in s['tasks'] if t['id'] in ['LT1','LT2','LT3','LT4','LT5','LT6','LT7','LT8']})"`.)
2. `../.venv-dev/bin/pytest tests -q` passes and `main/check.sh` prints "checks passed" on the
   current tree; `cd main && ../.venv-dev/bin/alembic heads` prints exactly one head; `bash
   main/docs-check.sh --strict` is clean.
3. Every finished task has its own commit prefixed `L:` (`git log --oneline | grep "^[0-9a-f]* L:"`).
4. A handoff entry for this session exists at the top of `.logs/handoff.md` (≤25 lines: what
   landed and how it was verified with the numbers from `dfp_l`, the migration id `b3c4d5e6f7a8` and
   the after-deploy commands in the brief's order, the Extime window file and the three running-list
   items, what is blocked and why, what rian must do), and `/checkpoint` has been run.
5. Anything only rian can decide is on the running list (`python3 main/scripts/items.py add
   --kind decide --by "Stream L" --blocks <task> --weight ...`) with the assumption you proceeded under,
   and the Extime `do` item and the refused-shops `decide` item from LT8 exist.
6. The rehearsal database `dfp_l`, any local uvicorn on 5433 and any `main/static` symlink are gone
   (`docker exec dfp-devdb psql -U dfp -d postgres -c '\l' | grep -c dfp_l` prints 0;
   `pgrep -af uvicorn` shows nothing of yours; `ls main/static` fails).

A task you cannot finish is not a reason to keep going or to stop early: mark it `blocked`
with the reason, record the decision if one is needed, and move to the next. If all tasks
are done or blocked, finish steps 2 to 6 and stop. If eight hours have passed, mark every
unfinished task `blocked` with the note "eight-hour cap; resume with /stream-l", drop `dfp_l`,
write the handoff, run `/checkpoint`, and stop.

## The goal line (rian pastes this after the first turn ends)
Start the session with `/stream-l`. When that first turn ends, paste this one line:

```
/goal Stream L of the Duty Free Professor build is complete: in import/progress.json every one of the task ids LT1, LT2, LT3, LT4, LT5, LT6, LT7, LT8 has status done, or blocked with a note saying why; main/check.sh has printed "checks passed" on the final tree and alembic heads prints exactly one head; every finished task has its own commit prefixed "L:"; a handoff entry for this session is at the top of .logs/handoff.md naming migration b3c4d5e6f7a8 and the after-deploy commands in order, and /checkpoint has been run; the rehearsal database dfp_l is dropped; anything only rian can decide has been added to the running list with main/scripts/items.py, including the Extime recrawl do item. Or stop after 200 turns.
```
