# Stream K13: the review that reasons

(written 2026-09-21 by Claude with rian, from the plan `.logs/planning/review-reasoning-2026-09-21.md`,
which is the design; this brief is the work list. Where the two differ, the plan wins and the
difference is written here before the code.)

**Runs:** in its own worktree and branch off master. Never deploy, never write to staging's or
production's database, never run a collection, never edit `.app.env` or `.env`, never edit
`import/proposals/`. Rehearse every data move on `dfp-devdb` (127.0.0.1:5433) from a fresh copy of
the newest dump in `backups/`, closing any inherited `running` collection row in the copy by
`status` (not `finished_at`).

**This stream does run a pass — on the copy only, in K13.8, as the proof.** Never on staging.

## The two rules that shape every task

1. **We are building the room, not doing the work in it.** No task decides a catalogue question. A
   fixture may hold a gendered name or a tube because the code must carry such cases; the test
   asserts the question is *raised, rated and grouped*, never what the answer is.
2. **Guidelines, not rules — and the obvious case is the point, not an exception.** The previous
   round failed by turning "be loud about new kinds" into machinery that made every question
   critical. Where you are tempted to write a rule that classifies a case, write guidance with
   examples and let the pass reason. If a mechanism would hand rian an answer instead of a
   question, it is wrong. **Test: would this have folded Hugo Boss into BOSS without asking?**

## What already exists (do not rebuild)

The ledger, the proposals store, load / sheet / approve / defer / counter / undo; the precedent
register and `app.cli precedents` (K12); `attention.py` and its five levels; the approval ladder and
bulk-by-group; the pass packet, fingerprint and `pass status`; the survey field; the evidence span
check; `deferred_notes` with `wants`; the brand gate and the redirect on confirm; `RowListings` and
its "not collected yet" wording; `plan-set.py --add`.

## Owns

`main/docs/REVIEW-PASS.md`, `main/docs/AI-REVIEW-GUIDELINES.md`, `main/docs/REVIEW-PROCESS.md`,
`main/docs/PRECEDENTS.md`, `.claude/commands/review-pass.md`, `main/app/services/attention.py`,
`precedents.py`, `pass_packet.py`, `proposal_rules.py` (the version only), the proposal-shaped parts
of `proposals.py`, `review_detail.py`, `app/models/decisions.py`, `app/models/schemas.py`,
`app/routers/review.py`, `app/cli_precedents.py`, one new alembic revision,
`main/web/src/components/review/*`, `main/web/src/pages/ReviewPage.tsx`, `main/tests/*`.

**Must not touch:** `app/services/collectors/`, `ingest.py`, `collected.py`, `merges.py`'s fold
logic, anything under `import/`, the accounts and kit files.

## Tasks

### K13.1 — The spirit, written down
Replace the "Two rules above every other" block at the top of `main/docs/REVIEW-PASS.md` with Part A
of the plan, **verbatim**. Rewrite `main/docs/AI-REVIEW-GUIDELINES.md` so every entry reads as
guidance with examples rather than a test to apply: keep what still holds, cut anything that
classifies a case for the reader, and add the examples from Part A. Bring `REVIEW-PROCESS.md`
section 4 into line with B1 and B2. Section 1 of `REVIEW-PASS.md` keeps "the packet and nothing
else" for catalogue data, and gains the research allowance and its limit: the world, never the
shops' pages.
*Verified:* `bash main/docs-check.sh` passes; the phrase "loud about a new kind is the job" appears
nowhere; a reader of REVIEW-PASS.md section 0 can state the three attention factors.

### K13.2 — Attention becomes the pass's judgement
One alembic revision, schema only, server defaults, nullable: `proposals.subjectivity`
(`varchar(12)`), `proposals.attention_why` (`text`), `proposals.held_behind_id` (`bigint`, FK to
`proposals.id`, for K13.5). In `attention.py`, delete the floor: no critical-by-construction for an
unregistered kind, no `LEAVE_HIGH_AFTER` gate, nothing that raises a pass's level. Keep the levels,
their descriptions, and add a reporting helper that returns the four things a card shows —
confidence, subjectivity, the computed consequence counts, and `attention_why`. The register's
per-kind decision count goes into the packet as reasoning material. `precedent` becomes required
only when `precedent_status` is set (B3): `PRECEDENT_MISSING` fires only then.
*Verified:* a row with no precedent and `attention: low` loads and stays low; a test names the old
behaviour (15 critical, 38 none on staging) in its docstring as the cost.

### K13.3 — The first precedent, and only it
Seed one register row through the existing path: a brand that renamed itself folds into its new
name, `set_by` rian, dated 21 Sep 2026, his words as the note, first case Paco Rabanne to Rabanne.
Record it in `main/docs/PRECEDENTS.md` if that file is the register's written face.
*Verified:* `app.cli precedents list` shows exactly one active row on a fresh copy; a test asserts
the seed is one row, so a later session cannot quietly add a second.

### K13.4 — Brand candidates in the packet
Add `brand_candidates` to the packet, per B5: shared product line keys weighted by how many brands
hold each key, name similarity (containment, initials, a spelling slip), and a listing whose
`listed_brand` names another brand row. Each candidate carries its signals and the counts on both
sides; none is filtered out by a rule that would decide it. Add a catalogue-wide form
(`pass candidates`, or `pass packet --brands`) so a general review starts at brands. When the packet
names a candidate pair, it carries both families' product lines, product variants and listings, so
the pass can reason and K13.5 can pre-compute.
*Verified:* on a copy of staging the nine pairs in B5 appear with rabanne/paco-rabanne first and
**hugo-boss/boss present**; the tequila and whisky pairs that share only a category word are ranked
below them or absent; the numbers go in the handoff.

### K13.5 — The next step, held behind a confident fold
A proposal may name `held_behind` in its file as the natural key of another row in the same file;
the loader resolves it to that row's id. A held row is not shown, not answerable, and not counted
in `pending`. Confirming the gate opens the held rows on the surviving brand, in the same batch, so
one undo reverses both. Rejecting it withdraws them, with the reason naming the gate. The existing
redirect carries rian to the survivor.
*Verified:* on the copy, a brand fold with held product-line rows shows `pending: 1` on **both**
brands; after confirm the held rows are open on the survivor and the alias's page redirects; after
an undo the held rows are held again, not lost.

### K13.6 — Who calls it what
On a brand-pair card, a table: one row per product variant, the brand name each shop wrote, the
shops that disagree first, with source links. Uncollected wording says so, in the words
`RowListings` already uses. Reachable from the card without leaving the page.
*Verified:* the Rabanne pair renders it; a product variant whose shops disagree sorts above one
where they agree; a variant with no collected wording explains itself rather than showing blanks.

### K13.7 — The survey leaves the brand page
Stop rendering `SurveyBlock` on the brand sheet. The survey stays on the pass record and stays
queryable. Keep the precedent groups only insofar as they are the bulk pile; if they read as a
report rather than a pile, make them read as a pile.
*Verified:* a brand sheet under a pass with a 19-kind survey renders no survey prose; the pass row
still carries it.

### K13.8 — Rabanne end to end, on a copy
On a fresh copy only: run the brand layer for Rabanne under the new spec, **using research** to
establish the rename, and let it pre-compute the held product-line step. Confirm the fold as rian
would. Report: the attention level the pass chose and its stated why; how many questions were
raised at each level; that the held step appeared on confirm; and the same spread for the eight
questions the old pass raised, side by side. Then prepare, **for rian to run, not for you**: the
migration command, and the withdrawal of the three passes written under the flaw
(`claude/2026-09-20/rabanne-product_lines-1`, `johnnie-walker-cross_divide-1`,
`lancome-cross_divide-1`).
*Verified:* the numbers are in the handoff; nothing was written to staging; the copy is dropped
afterwards.

## Recorded, not built

Add to the running list with `items.py add --kind decide --by "Stream K13"`: the live "run a pass"
button will need a web search tool in its API call, or a pass loses the confidence that comes from
checking a claim about the world.
