# 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, the brand candidates), `main/app/services/proposals.py` (the loader and its refusals),
`main/app/services/precedents.py` (the register), `main/app/services/attention.py` (the levels and
what a card reports), `main/docs/REVIEW-PROCESS.md` (the boundary, the ladder, the file format),
`main/docs/AI-REVIEW-GUIDELINES.md` (the guidelines), `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 for everything about the
catalogue 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.

## 0. How a pass thinks

> **The collector and the reviewer have opposite jobs.** The collector follows rigid rules, keeps
> the raw words, and when unsure keeps things apart. The reviewer reasons about what the collector
> kept apart and recommends which of those are really one thing.
>
> **The reviewer is an employee preparing decisions for a manager.** The manager answers for every
> decision but cannot read every case. So the employee sorts: most decisions go on a pile to be
> signed in bulk, and a few are brought to the manager with the evidence, the reasoning and a
> recommendation the manager is free to overrule. Sorting well is the job. Escalating everything is
> as much a failure as escalating nothing.
>
> **Guidelines, not rules.** The guidelines say what to look for and give examples: the same name
> spelled or capitalised differently, in another language, with a word added or dropped, in a
> shop's shorthand, or a brand that renamed itself. These are usually the obvious cases, and the
> obvious cases are what the review is for. But *usually* is doing work in that sentence. The pass
> reasons about each one, and when an example does not fit — a shorter name that is really a
> different line of the same house — it says so.
>
> **Find out, do not guess.** A pass may research the world — a brand's history, who owns what,
> what a product is — and cites what it found. It never reads the shops' pages itself: what each
> shop wrote comes from the packet, which the collection gathered under its own rules.
>
> **Attention is how loudly to call the manager, and it is a judgement** made from three things,
> which the card states:
> - **confidence** — how sure the pass is, after looking and researching;
> - **subjectivity** — whether this is a fact (the house renamed itself; the article number is the
>   same) or a call someone has to make;
> - **consequence** — how much the answer changes.
>
> Confident, factual and small goes on the pile. Uncertain, subjective or large goes up, and the
> more of those it is, the louder. *Paco Rabanne renaming itself Rabanne is confident and factual,
> but the fold moves 24 product lines and 54 product variants, so it is medium. Whether BOSS and
> HUGO are one brand is a real call, so it is high or critical and gets the longer look.*
>
> **Precedents are the manager's rulings, and they are how the reviewer learns.** A judgement call,
> once answered, is recorded with the manager's note, and the next case like it is less subjective
> because it has been ruled on. The first few after a ruling may still be raised, so the manager can
> see it applied the way it was meant; as the rulings hold, those cases move to the pile. A
> precedent is applied with judgement too: a case that looks like one but differs in a way that
> matters is raised, saying how it differs.
>
> **Think harder where it matters.** On a genuinely subjective question the pass does what a good
> employee does before knocking on the door: what exactly is the difference, how it shows in our
> listings, which shops call it what and where they disagree, what the options are and what each
> does to the catalogue. Then it recommends one.
>
> **When the answer is near certain, prepare what comes next.** If the pass is confident an outer
> question will be confirmed — a brand fold — it also writes the next step for the combined result,
> held back until the confirm, so the next decisions are already waiting. A rejection throws the
> held work away.
>
> **It never decides.** The pass recommends, the manager decides, and the precedents record what
> was decided.

## 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. **The packet is the whole of what a pass knows
about the catalogue.** What a pass may add is knowledge of the world: a brand's history, who owns
what, what a product is, checked with a search and cited in the reason. It never reads a shop's
page: what each shop wrote is in the packet, gathered by the collection under its own rules, and a
pass that fetched a retailer's page would be a collector without the permission record.

| 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 ruling a person has made, active or overturned, with the note, the first case, and how many decisions have followed it; reasoning material, never a gate; an overturned one lists the decisions made under it |
| `brand_candidates` | other brand rows this one may be the same house as, with the signals that put each there and the counts on both sides (section 3, layer 1); a hint the pass judges and may reject, never a conclusion |
| `principles` | `AI-REVIEW-GUIDELINES.md` as text |
| `attention` | the five levels, what each means, and the three things a card states |

## 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:

- `attention`: the pass's judgement of how loudly to call the person (section 0), one of the five
  levels. **The loader never changes it.** Beside it, `subjectivity` (`fact` | `settled` |
  `judgement`: a fact of the world or of the words, a call a ruling has already made, or a call
  someone has to make) and `attention_why`, one line naming the three factors: how sure, how
  subjective, how much moves. The consequence is computed by the system from what the answer
  moves and shown beside the level; the pass reasons from the same counts, which the packet holds.
- `precedent`: a register slug, **only when the row sets or follows a ruling** (`precedent_status`
  `sets` on the one lead that asks a new kind of question, `follows` on a row that applies a
  ruling the register holds). A routine recommendation carries neither: it is the pile, not a
  precedent. `PRECEDENT_MISSING` fires only on a row that names a `precedent_status` without a
  slug; `PRECEDENT_SET_TWICE` when two rows in one file set the same slug.
- `follows`: on a follower, the natural key of the lead it goes with; rated `none`. A follower is a
  row whose answer is the lead's answer, said by name in the lead's reason.
- `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).
- `held_behind`: the natural key of another row in the same file this one waits on
  (`HELD_BEHIND_MISSING` when no row of the file has that key; `HELD_BEHIND_INVALID` behind itself or
  behind another held row). A held row is not shown, not answerable and not counted as pending;
  confirming the row it names opens it on the surviving side in the same batch, so one undo holds it
  again; rejecting withdraws it with a reason naming the gate; deferring leaves it held.

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 brand (`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, two example listings each, and the question each produces. Write it to the
   header's `survey` as `[{kind, precedent, registered, examples, question}]`. The survey is the
   pass's working note and the record of what it considered; it stays on the pass record and is
   not drawn on the brand page. It rules on nothing. 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 is about to call routine, so the sorting is a decision it made and not
     one it skipped.
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. **Sort.** Most recommendations go on the pile: confident, factual, small (section 0). A question
   that sets a new kind of ruling, or applies one the register holds, names its slug; one lead per
   slug, its followers name it in `follows` and are rated `none`, and the lead's reason says which
   rows it holds back, by name. When unsure whether two questions are one kind, they are two. A
   precedent is applied with judgement: a case that looks like one but differs in a way that
   matters is its own lead, and the reason says how it differs.
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. **State the attention** from the three factors and write them in `attention_why` (section 0).
   Research first where a fact would settle it. The loader records the level as given.
   Where the pass is near certain of an outer answer, write the next step for the combined result
   and hold it behind that row (`held_behind`, section 2); a rejection throws the held work away.
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.
