# Stream K11: the AI pass is the review; the programmatic stage stops asking

(written 2026-09-17 Thu from rian's first real reading of `/review` on staging, and from his own
words in `.logs/planning/catalogue-walkthrough-2026-09-16.md` W10, W11, W14 and W16)

**Runs:** in its own worktree and branch off master, **after K9 and K10 are merged** (rian merges
those; this stream starts from the merged master). Never deploy, never write to staging's or
production's database, never run a collection, never edit `.app.env` or `.env`. Rehearse every data
move on `dfp-devdb` (127.0.0.1:5433) from a fresh copy of the newest dump in `backups/`.

**This stream does NOT run the AI pass.** That is a separate session (`/review-pass`) on two or
three brands rian names, run after he has seen the interface this stream builds. Building the
interface and running the pass are deliberately separate so he can judge the interface on a small
batch before any sweep.

## Why

Rian spent two days reviewing `/review` believing he was looking at AI proposals. He was not.
Every row in `proposal_passes` is `kind = rule`, `generator = rule`, written in one instant by
`backfill rule_proposals`. **The AI pass has never run.** Worse, he should not have been shown that
output at all. His model, from the walk-through, verbatim:

> **W10.** *"we live with the problem of many different lines at first, so there's not really a
> rule that takes place at collection time if there's a good chance we'll get it wrong. instead, we
> do an AI pass through to look for recommended corrections. So the AI pass would present a
> recommendation that we have one product line called Rouge Allure. then below that it lists all
> the listings in a table with proposed attribute population. The human scans through it, makes any
> corrections, and approves."*

> **W11.** *"if there's a potential for making a mistake with an algorithmic analysis, opt to just
> record the raw data in the most relevant field (like variant name) rather than risk populating it
> wrongly. I think an AI pass with reasoning can do a better job of suggesting the correct brands,
> product lines, product variants, and attributes, then a human can just spend time going through
> and confirming or correcting."*

**How it went wrong.** W14 offered four options; B ("certain-only keys; grouping proposed by the AI
and confirmed by a person") and D ("keep the lists as proposals, not actions") were alternatives.
Rian's recorded position was that he *"leans to B or D"* and delegated the balance. The plan chose
**both** — "option B for the key, option D for the lists". Under B alone an uncertain word list
simply does not act, and the extra product lines wait for the pass. Adding D turned those same
guesses into about 938 questions in rian's queue, which arrived before the pass had ever run and
were therefore the only thing he had ever seen. No document states that consequence, so there was
nothing to catch.

## What rian ruled, 17 Sep (this is the spec)

1. **Programmatic decisions are allowed, must err toward SEPARATE brands and product lines, and are
   never reviewed by him.** Many duplicate lines and brands is the correct output of that stage.
2. **The AI pass does the correction work** a human would do in an Airtable-style view of
   `/collectors` (raw data beside editable fields), which is real but impossible by hand at this
   scale, and presents it for confirmation. Trust compounds until most answers are "approve all".
3. **Judgement calls are raised per case, never pre-decided as blanket defaults.** Rian may approve,
   **overturn**, or **defer with a note**; the note feeds the next pass; the accumulated consensus
   *becomes* the reasoning rules. His worked example, on which he is genuinely undecided: a refill
   serving several product lines is not the same case as one line with one refill; a gift set
   spanning several lines is not the same as three shades of one line.

## What already exists (do not rebuild)

- The decisions ledger, the proposals store, load / sheet / approve / reject / undo (K2, K4).
- `proposals.withdraw(pass_name, reason, by)` already does exactly what K11.2 needs.
- `services/proposal_rules.py`'s generators. **They are not deleted**; they stop being questions for
  rian and become hints an AI pass may read. That was D's real value.
- The `/review-pass` skill, and `import/proposals/2026-09-17-chanel-rehearsal.json` as a worked file.
- `brand` is already a decidable entity with `name`, `alias_of`, `hidden`, `indexed`, and
  `apply_brand_alias` already cascades its lines and variants.

## Tasks, in order (commit prefix `K11:`)

### K11.1 `REVIEW-PROCESS.md` changes role (version 4)

The document currently prescribes blanket per-vertical defaults in section 2, decided in advance. It
becomes **a record of consensus as it forms**. Rewrite section 2 accordingly: each entry says what
was decided, from which case, and what is still open. The set / coffret / kit / gift pack line is
the worked example: it is **not** a default any more; it is a question the pass raises per case,
with rian's two distinctions above written as what the pass must weigh. Add:

- **A brand section**, which the document has never had: when two brand rows are the same house and
  when they are not; that a rename with no shared words (Paco Rabanne to Rabanne) is exactly the
  case no rule can find and the pass must; that a containment overlap (Armani inside Giorgio
  Armani) is a candidate, not a conclusion.
- **The display-name policy**, as rian set it: a brand's display name is what the brand itself would
  use, chosen by the pass, **never** by whichever spelling is currently most common (that changes
  with every collection). Set it where nothing is set; **propose against** a human value rather than
  overwrite it (`against_decision_id` already models this).

Bump the version line and say so in the handoff. It is a rules version, so
`tests/test_certain_boundary.py` must still pass; section 1, the certain boundary, does not change.

### K11.2 The word lists leave the human queue and become input to a pass

`/review` must show **nothing that is not a session pass**. The rows are not deleted: withdrawal is
reversible and keeps them. Build:

- the read path that gives a pass, per brand, what each list would have suggested and why
  (`proposal_rules` already generates it; this is a read, not a new store);
- the sheet and the brand index excluding non-session passes by default.

**Do not withdraw on staging.** Hand rian the exact commands in the handoff
(`app.cli proposals withdraw --pass rule:<list>:6 --reason "..." --by rian`, thirteen of them).

### K11.3 `brand_key` stops folding, and the trademark glyphs stop splitting

`normalize.brand_key` still pops trailing words from `_BRAND_TRAILERS`, so "Tanqueray Gin" and
"Tanqueray" become one brand row with no decision behind it. That is the programmatic stage
**joining**, against rule 1. Remove the trailer fold from the key. Separately, the key runs NFKD then
strips non-ASCII, and the trademark and registered glyphs decompose to the *letters* "tm" and "r"
and survive, which is why `CIROC` with a trademark glyph is a different brand row from `Ciroc`.
Strip them before folding.

Both re-key brands. **Measure on a copy first**, report a table of which rows split and which merge,
and **stop and report rather than proceed** if any existing decision would be disturbed. Coordinate
with K9's per-vertical scoping if it has landed: this removes the fold from the *key*, it does not
remove K9's list.

### K11.4 Defer with a note, and the note reaching the next pass

Today a row is approved or rejected-with-a-sentence, and **nothing ever reads the sentence**. Add a
third answer, `deferred`, carrying rian's note:

- a migration adding `deferred` to `ck_proposals_status` (it is a CHECK list; rehearse up, down, up);
- a deferred row leaves the queue, is never applied, and is re-proposed only by a pass that has read
  its note;
- the read path that gives a pass every note bearing on the brand or the question in front of it.

This is the loop that turns rian's answers into the reasoning rules. Without it every pass starts
from zero and the set/kit question is asked identically forever.

### K11.5 What a pass emits: the product line table

Extend the proposals file so a pass proposes a product line **and, beneath it, every listing that
would sit in it with the attribute population proposed for each**: the decided display name, the
quantity variations, the shades, the shops and prices with links to the source, and each proposed
fold called out with its reason. It is a view over rows the schema already allows; do not invent a
parallel store. Rian's example, in his words: a table for La Vie est Belle showing its decided
display name, quantity variations, shades, airport and price stats with links he can click to check
the source, highlighting that the suggestion is to collapse Gift Set and Refill Set into a
selectable attribute, and naming the industry-standard word for it.

### K11.6 `/review` renders that table as the unit of review

The product line table replaces per-row questions as the primary surface. Approve, overturn, or
defer-with-a-note **per row and for the whole line**. Keep what master already fixed: a row names
what it is about and asks its question in a person's words, and a pair shows both sides by name.

### K11.7 Tests, rehearsal, hand-back

The table renders from real staging-shaped data on a copy; a deferred row with a note survives a
second pass and demonstrably changes what that pass proposes; the brand re-key is reported as a
table before anything is applied; `main/check.sh` prints "checks passed"; a five-line handoff naming
the withdraw commands rian must run and the migration he must apply.

## Owns

`main/docs/REVIEW-PROCESS.md`, `main/app/services/proposals.py`, `main/app/services/proposal_rules.py`
(its read path, not the generators' logic), `normalize.brand_key`, `main/app/routers/review.py`,
`main/web/src/pages/ReviewPage.tsx` and its CSS, `main/web/src/components/review/*`, the new
migration, and tests for all of the above.

## Must not touch

Staging and production, `.app.env`, `.env`, `import/proposals/`, the certain boundary in
`REVIEW-PROCESS.md` section 1, `services/decisions/`'s writer and undo, K9's split service and its
trailer list, K10's tab shell beyond mounting into it, and the `FORMAT_WORDS` membership: **rian has
ruled the opposite of the earlier proposal**, so set, gift, duo and refill stay on the list, to be
recognised as a possible *attribute*, never folded into the bottle's line by a rule.

## Not in this stream

Running the AI pass (a separate session, on brands rian names); the brand header UI from the earlier
K11 draft, which was a view onto stage-1 output and is superseded by K11.6; the deploy.

## Acceptance

- `/review` shows no rule pass, and the brand index agrees with the sheet.
- `REVIEW-PROCESS.md` is version 4, records consensus rather than prescribing defaults, and has a
  brand section and the display-name policy.
- A deferred row with a note changes what the next pass proposes, proven by a test.
- The brand re-key is measured and reported, not applied.
- The product line table renders on a copy, with approve / overturn / defer per row and per line.
- `main/check.sh` prints "checks passed"; nothing deployed; staging untouched.
