# The review pass: the specification any model executes

Sources of truth: this document, `main/app/services/pass_packet.py` (the packet, the fingerprint,
the status), `main/app/services/proposals.py` (the loader and its refusals),
`main/app/services/precedents.py` (the register), `main/app/services/attention.py` (the floor),
`main/docs/REVIEW-PROCESS.md` (the boundary, the ladder, the file format),
`main/docs/AI-REVIEW-GUIDELINES.md` (the principles), `main/tests/test_review_pass_spec.py`. The
session skill `.claude/commands/review-pass.md` is a thin wrapper over this file.

**A pass is a function from one packet to one file.** It runs on staging today with a Claude session
driven by hand, and live from a button or a schedule through an API. So it uses the packet and
nothing else: no database reads, no conversation history, no memory of a previous pass, no document
this file does not name. If the packet lacks something the proposals would need, the pass says so in
its `note` and stops. The same packet read by another model must produce a file the loader accepts.

**Two rules above every other.** The pass builds nothing that decides a catalogue question; it
raises the question, names its kind, and cites the words. And being loud about a new kind is the
job: the person approves in bulk, so a new kind rated routine costs every row of that kind silently.

## 1. Inputs: the packet

```
python -m app.cli pass packet --brand <slug> [--layer <name>] > packet.json
```

One JSON document. Its fields, and what each is for:

| field | what it is |
|---|---|
| `process_version`, `rules_version` | the versions the file must stamp |
| `brand` | the brand row and its family (aliases), by natural key |
| `layer` | the layer to work: the caller's, else the outermost with an open proposal, else `null` |
| `layers` | the five layers in order (section 3) |
| `state` | the brand's state from `pass status`: `never`, `open`, `deferred`, `parked`, `settled`, `unsettled` |
| `fingerprint` | the listed-words fingerprint the pass worked from; a settled file stamps it |
| `product_lines`, `product_variants` | every live row of the family, each variant with its listings |
| a listing | its listed columns exactly as the shop wrote them; `has_fragment`; `collected_name` where no fragment was kept; the retailer's population |
| `populations` | per retailer: whether it publishes listed words, whether it publishes barcodes |
| `cross_divide_candidates` | pairs across the barcode divide the pass judges (section 3, layer 2) |
| `hints` | what every word list would have suggested, with its reason; input, never a question |
| `notes` | every note a person left on a deferred or rejected question, with `wants` |
| `parked` | questions parked to the running list; a pass never asks these |
| `precedents` | the register: every kind of judgement answered, active or overturned, with counts and shapes; an overturned one lists the decisions made under it |
| `principles` | `AI-REVIEW-GUIDELINES.md` as text |
| `attention` | the levels and the threshold that lets a kind leave `high` |

## 2. Outputs

One proposals file per brand per pass, in the shape of `REVIEW-PROCESS.md` section 6, loaded with
`python -m app.cli proposals load --file <path> --check --as <username>` and then without `--check`.
Every row carries, beyond the fields section 6 already requires:

- `precedent`: the kind of judgement the row is, as a register slug (`membership:refill-one-line`,
  `cross-divide:same-bottle`, `name-a-product-line:translation-pair`). Required from process
  version 6 (`PRECEDENT_MISSING` otherwise). A slug the register does not hold is a **new kind**
  and is critical by construction.
- `precedent_status`: `sets` on the one lead that sets a new slug (`PRECEDENT_SET_TWICE` otherwise),
  `follows` on every row under a registered slug.
- `follows`: on a follower, the natural key of the lead it goes with; rated `none`.
- `answers_deferred`: the uid of the deferred note the row answers, where there is one
  (`DEFERRED_NOT_READ` otherwise).
- `evidence[].source` may be `collected_name` **only** on a listing with no fragment
  (`EVIDENCE_HAS_FRAGMENT` otherwise).

And on the pass header: `survey` (section 4), `verdict` (`proposals`, or `settled` with zero rows),
`fingerprint` (the packet's).

Nothing else is written. A pass never approves, rejects, defers, undoes, deploys, collects, edits a
document, or runs a command this file does not name.

## 3. The layers, outermost first

1. **brands**: two rows that are one house (`REVIEW-PROCESS.md` 2.2).
2. **cross_divide**: a barcoded product variant with no listed text and a name-bearing one with no
   barcode that read as one bottle at one size. The packet's candidates are the input; the pass
   judges each and proposes a pair decision with evidence on the side that has words. This runs
   before product lines because it changes what the lines contain.
3. **product_lines**: two lines that are one; a set, refill or coffret finding its home; the name.
4. **product_variants**: two variants within a line that are one bottle.
5. **attributes**: the values that tell variants inside a line apart.

A pass proposes in ONE layer: the packet's `layer` when it names one; otherwise the outermost layer
in which the pass finds a question. A decision in a layer may raise questions only in the layers
after it, which is what makes the passes converge.

## 4. Steps, in order

1. **Survey.** Before any proposal, list every KIND of difference visible between rows that would
   otherwise group, registered or new, two example listings each, and the lead question each
   produces. Write it to the header's `survey` as `[{kind, precedent, registered, examples, question}]`,
   new kinds first. The survey is what makes a new kind loud; it never stops the pass. Look, in
   general terms, for:
   - a word present on some sibling names and absent on one, in a set that would otherwise group.
     **Never assume the absent one is the parent**; list the marked and the unmarked and ask which
     the unmarked one is;
   - a value stated on some siblings and silent on others (an age, a strength, a size);
   - any hint list that fired (audience, pack, format, age, region, cask, shade, skin-type words)
     and whether the register holds a precedent under that shape;
   - a shop whose wording differs in kind from the others (shorthand against retail names);
   - two languages for one thing, a category descriptor appended, a packaging word: each its own
     kind, never grouped as noise;
   - anything the pass would otherwise call routine and cannot find a precedent for.
2. **Read the notes and the parked list.** A note with `wants: a different suggestion` is what to
   propose instead; a parked question is not asked.
3. **Choose the layer** (section 3).
4. **Group by precedent.** For each question, name its slug from the register, or mint a new slug
   when no registered one fits. When unsure whether two questions are one kind, they are two. One
   lead per slug; its followers name it in `follows` and are rated `none`. Say in the lead's reason
   which rows it holds back, by name.
5. **Write the proposals** (section 2). Every value cites a span or a `collected_name`; a brand
   rename says its reason plainly. A reason is an argument in one or two sentences, never a
   citation of a document or a section.
6. **Rate attention** by what the answer does, never by how sure the reading is. The loader floors
   it and may raise it; it never lowers it.
7. **Self-check** with `proposals load --check`; a refusal is an instruction. Fix, then load.
8. **Report**: the brand, the layer, the survey's count of new kinds, the counts, the held-back
   by name, and the sheet address `/review?brand=<slug>`. If no layer holds a question, load a
   file with zero proposals and `verdict: settled`, one line of reason, and the packet's fingerprint.

## 5. Stopping conditions

Stop after loading one layer for the named brands. Stop with a note and no file when: the packet's
`state` is `parked` or `settled`; the packet lacks data the proposals would need; a note asks for a
different suggestion and the pass cannot find one (say so; the loader parks the question after its
second deferral, `PARKED_TO_RIAN`).

## 6. Done

`python -m app.cli pass status` prints, per brand, `never | open | deferred | parked | settled |
unsettled`, and one line: `done: 0 open, 0 deferred, 0 wants, 0 parked; N brands settled`, or the
counts that remain. Passes run until that line reads done. A settled brand un-settles when its
listed words change (a sweep, an arrival), when this file's process version changes, or when the
identity rules version changes.
