---
type: plan
---

# The review we go live with: the plan

**Written 18 Sep 2026 by Claude (Fable), from the brief at `review-process-rebuild-brief.md`, the
simulation log of the same day, `REVIEW-PROCESS.md` v5, the guidelines, the pass skill, the review
code (`attention.py`, `proposals.py`, `appliers.py`, `undo.py`) and staging's current state, read
read-only.** Implemented by Stream K12 (`streams/K12-review-golive.md`).

The sequence after the stream is fixed: **K12 runs → a collection sweep → passes until no decisions
remain on the listings mosiah holds.** That result is the launch catalogue.

This plan builds the room. It rules on nothing in the room: no gendered bottle, no concentration,
no tube. Where a catalogue question appears below it appears as an example of a *kind of question
the process must notice*, never as an answer.

---

## Part A. Three findings that change the brief

### A1. The sweep will not refresh the barcode-rich side, so the review must cite what it holds

The brief says the listed-word gaps are staleness and a sweep closes them. That is true for Extime
(7,036 listings, 200 fragments; last run ok on 11 Sep) and **false for exactly the population the
structural finding is about**. Read from staging today:

| source | last run | why |
|---|---|---|
| heinemann-global, iceland-duty-free, sydney-duty-free, bordershop-scandinavia | blocked, 23 Aug | robots.txt `Disallow: /en/global/search/results` |
| dubai-duty-free | ok 22 Aug; 403 to our declared identity since | edge refusal (Decision 3: no bypass) |

A block is a refusal. Those five sources hold **5,737 listings with no fragment and no listed
text**, and under `agents.md` they are never contacted again until a partnership. So after the
sweep, the barcode-rich side still has no listed words, and section 3's rule ("never propose a
value the raw text does not contain") makes every one of its 3,847 variants uncitable, which is
what blocked glenfiddich on 18 Sep.

The text is not gone. It is in `product_variants.name`, built by the parser from the feed's own
fields (`brand`, `name`, `contentUnit`): *"Jura 13yo 40% 1L GP\*"*, *"Sailor Jerry Twinpack 40%
2x1L"*. That is the shop's wording with the provenance lost, not a machine guess.

**Decision for the stream:** a proposal may cite `source: "collected_name"` for a listing that has
**no fragment** (`listed_record_id IS NULL`); the loader refuses it on a listing that has one; the
sheet labels it *"as collected, no fragment kept"*; staleness is checked against the variant's name
(which changes only by decision). The listed layer stays pure. Cross-divide merges already load
with evidence on one side only; this makes the name side of the divide citable too.

**Not doing:** a derived "normalised name" on the barcode-rich side. It would be a machine value
sitting in the standard layer looking like a fact, the exact thing "empty beats guessed" exists to
prevent. The reading of *Joh.Walk Bl L PET* as Johnnie Walker Blue Label belongs in a pass's
reasoning with the raw string cited, never in a column.

### A2. Attention gates nothing, and nothing independent checks it

`_resolve_scope("all")` approves every open, non-spot-check row regardless of level. The five
levels exist and the page shows them; bulk approval ignores them. And the only precedent record is
`attention._precedents()`: a set of keys computed from *words in the reason text*
(`"gift"`, `"refill"`, `"set"`), so a pass that phrases a reason differently invents or dodges a
precedent by accident. Nothing names a precedent; nothing counts decisions under one; nothing can
say "this row is unlike the cases its precedent was set on".

### A3. The refused sources also decide the value order

With five sources frozen, "comparison gain" (section 5) is dominated by cross-divide matches
between a frozen barcode-rich row and a swept name-rich row. The order the brief gives is right;
the measure behind it should count cross-divide candidates, which K12.5's generator produces.

---

## Part B. The six questions, answered

### B1. Naming: `high` wins, and `important` is never written

Five levels stay (`critical / high / medium / low / none`): renaming costs a migration, a CHECK
constraint, every label and the guide, for one word. What changes is that the levels become
**three gates and a marker**, stated in one table everywhere (process doc, guide tab, card):

| level | bulk? | to answer it |
|---|---|---|
| **critical** | never | a click **and a note**; the note is refused empty (`NOTE_REQUIRED`) |
| **high** | never | a click; a note is optional, except "Suggest something else", where the note is the answer |
| **medium**, **low** | as a group | one confirmation for the group, with an optional note written on the batch and shown on every row it covered |
| **none** | with its lead | a follower: it says which question it follows and goes with that answer |

### B2. Who checks the rating: three checks, none of them the pass's own opinion

The brief is right that a pass rating its own novelty is the hole bulk approval drives through.
The answer is not a second model (same blind spots, double the cost). It is mechanical floors the
loader applies, which a pass can raise and can never lower:

1. **The precedent register.** Every proposal names the *kind of judgement* it is
   (`precedent: "membership:refill-one-line"`) and whether it **sets** that precedent or
   **follows** it. The register (K12.3) is a table of every precedent ever set: slug, vertical,
   the question as first asked, the answer, rian's note verbatim, the case, the count of
   decisions made under it, and its status. **A slug not in the register is critical by
   construction.** The pass cannot under-rate a new kind unless it files it under an old one.
2. **The unlike-its-precedent check**, for exactly that dodge. When a row follows a registered
   precedent, the loader computes its *shape*: entity type, field, vertical, which hint lists
   fired on its listed text (`audience_fold`, `pack_words`, `format_words`, `age_words`, …),
   and which of those words appear. Any element never recorded on that precedent's earlier cases
   marks the row **unlike its precedent**, floors it at `high`, and says which element. A row
   named *"for men"* filed under `membership:plain` trips `audience_fold`, which no plain
   membership decision has ever carried. The check knows nothing about gender; it knows the
   register has never seen that shape, which is the whole point.
3. **The person**, through what already works: the held-back named on the lead question, and the
   seeded random sample. Plus one addition: a **bulk confirmation lists the unseen words**, the
   words in the group's listed text that no decided case in that vertical has ever contained.
   That list is long on the first brand and short by the fiftieth, which is the curve the brief
   asks for, produced by counting and not by trust.

And the loudness, so novelty is not merely gated but seen: a pass's first output per brand is a
**survey** (B7), the sheet opens with it, the brand index sorts brands with new kinds first, and a
load with new kinds sends one Telegram line.

### B3. The bulk group is the precedent, not the brand or the product line

Rian's ladder bulk-approves "a group" of medium and low. The right group is **everything one
answer decides**: a precedent's lead question and the rows that follow it. That is what "one
question per precedent, held-back named" already is; K12 makes it the unit the sheet groups by and
the unit `approve` accepts (`scope: {"precedent": slug}`). A product line group stays as a view.
"The rest" (`scope: "all"`) is kept for the steady state and is **refused while any critical or
high row on the sheet is open** (`BULK_BLOCKED_BY_OPEN_LEAD`, naming them), so it cannot be the
first thing pressed.

### B4. Convergence: five bounds, and a defined done

1. **Layers are ordered and never re-open outward.** Brands → cross-divide product variants →
   product lines → product variants within a line → attributes. A decision in a layer may raise
   questions only in inner layers, and the layers are finite.
2. **A question has at most three rounds.** Proposed; deferred or "something else" with a note;
   re-proposed citing the note; deferred again → the loader **parks it as a `decide` item on the
   running list** (`PARKED_TO_RIAN`) and no pass proposes it again until that item is answered.
   Defer loops cannot be infinite.
3. **Unchanged words are never re-asked.** A listing whose listed words are unchanged since its
   last decision is not proposed again (already the rule; the settled stamp enforces it per brand).
4. **A pass must shrink the open set or declare settled.** A pass on a brand ends either with
   proposals in one layer, or with an explicit `verdict: settled` row on the pass (zero proposals,
   a one-line reason), stamped with the process version, the rules version and the brand's
   **listed-words fingerprint** (a hash over its live listings' listed text). Any of the three
   changing un-settles the brand.
5. **Arrivals re-open only what they touch.** A new listing un-settles its brand alone; the
   standing arrival pass proposes it to the nearest approved product line at the floor B2 gives.

**Done** is `app.cli pass status` printing: every brand settled at the current versions, zero
deferred rows, zero `wants` outstanding, zero parked decide items open. That line is the terminal
condition the brief asked for, and it is measured, not felt.

### B5. Generate the precedents; keep the principles by hand

Half yes. Two documents were doing two jobs under one name:

- **Principles** (how a pass behaves: cite or stay silent, say which case, rate by consequence,
  err toward separate). Stable, few, written by a person. They stay in
  `AI-REVIEW-GUIDELINES.md`, and a pass **stops rewriting that file**; a principle changes by a
  commit with a reason.
- **Precedents** (what rian decided, case by case). These are the notes, and with a note required
  on every critical decision they are complete by construction. They are **generated** from the
  ledger and the notes into the register (K12.3), rendered on the guide tab and exported to
  `main/docs/PRECEDENTS.md` by `app.cli precedents export` for reading and diffing. A pass reads
  the register from its packet. Nothing about a precedent lives in a model's memory or in a file
  a pass remembered to edit.

The existing guidelines that read like precedents ("concentrations are attributes", "one line per
age") came from the walk-through, not from a ledger decision; they stay as principles that state
the default the pass proposes when the text gives no reason otherwise, which is what section 2
says they are.

### B6. Correcting a precedent later: overturn, never rewrite

A precedent has an id, so it can be overturned as one act: `app.cli precedents overturn <slug>
--note "<the new answer>" --by rian` (and the guide tab's button). Overturning:

- writes a decision on the precedent itself (undoable like any other);
- **regroups nothing** (the standing rule: a changed decision changes what is proposed next, never
  what is approved);
- marks every decision made under it as *made under an overturned precedent*, which the next pass
  reads and answers with proposals **against** those decisions (`against_decision_id`, shown as
  disagreements, never applied); rian approves those as one precedent group.

So a change of mind after fifty decisions is: one overturn, one pass, one bulk confirmation, all
three undoable. Undo-batch remains for a mistake noticed at the moment; overturn is for a mind
changed later.

### B7. The unknown, made a step rather than a hope

The brief's sharpest requirement gets a mechanism in the pass itself, before any proposal: the
**survey**. For the brand in front of it, the pass lists every *kind* of difference it can see
between names that would otherwise group, and for each says whether the register holds a
precedent for it or not. The survey is a structured field on the pass row, shown at the top of
the sheet, with new kinds first and in red. The spec (Part C) names the shapes it must look for
in general terms (a marker word present on some siblings and absent on one; a stated value on some
and silence on others; a word a hint list flags; a shop whose wording differs in kind from the
others). **The unmarked member of a set** is the spec's standing example of why "absent = parent"
is never assumed: the pass lists the marked and unmarked members and asks which the unmarked one
is, at critical, and the answer becomes a precedent scoped to what rian's note says it covers.

None of this decides gender, concentration or packaging. It makes sure the question is asked once,
loudly, before a bulk button exists to hide it.

---

## Part C. What the pass is: the model-independent specification

Installed by K12.7 as `main/docs/REVIEW-PASS.md`. The session skill `.claude/commands/review-pass.md`
becomes a thin wrapper: where to run, how to commit, and "follow the spec". Live, "run a pass"
through an API is the same spec with the packet piped to a model and the file loaded back.

### C1. Inputs: one packet, nothing else

`app.cli pass packet --brand <slug> [--layer <name>] > packet.json`. One JSON document holding
everything a pass may know:

- the process version and rules version it must stamp;
- the brand row(s) of the family, live product lines, live product variants, every listing with
  its listed columns, its `collected_name` when it has no fragment, its shop and retailer, barcode,
  quantity, attributes, and which population its retailer is in (has listed words / has barcodes);
- the hints (`proposals hints --json`) including the cross-divide candidates;
- every note bearing on the brand (`proposals notes --json`), with `wants` and the round count;
- the precedent register (every active and overturned precedent, with counts);
- the principles (`AI-REVIEW-GUIDELINES.md` as text);
- the brand's open, parked and settled state, and the layer the caller asks for (default: the
  outermost with open questions, computed by `pass status`).

A pass uses no other source. No database reads, no conversation history, no memory of a previous
pass. If the packet lacks something the pass needs, the pass says so in its `note` and stops.

### C2. Steps, in order

1. **Survey.** Write the survey (B7) before anything else. Every kind seen, registered or new,
   with two example listings each. A brand with a new kind goes on regardless; the survey is what
   makes the new kind loud, not what stops the pass.
2. **Choose the layer.** The outermost with open questions, unless the packet names one. Propose
   in that layer only. Brand questions first; then cross-divide product variant pairs (a
   barcode-rich row and a name-rich row that read as one bottle at one size); then product lines;
   then product variants within a line; then attributes.
3. **Group by precedent.** For each question, name its precedent 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. For each slug: one **lead** proposal (`precedent_status: sets` if the slug is new,
   `follows` otherwise) and its **followers**, each naming the lead (`follows: <lead's key>`),
   rated `none`.
4. **Write the proposals** under the file rules (REVIEW-PROCESS.md section 6) with the new
   fields: `precedent`, `precedent_status`, `follows`, and `answers_deferred` where a note is
   answered. Every value cites a span or a `collected_name`; a brand rename says its reason
   plainly instead. A reason is an argument in one or two sentences, never a citation of a
   document.
5. **Rate attention** by consequence; the loader will floor it and may raise it, never lower it.
6. **Self-check** with `proposals load --check`. Fix what it names; a refusal is an instruction.
7. **Load**, then print the brand, the layer, the survey's count of new kinds, the counts, the
   held-back by name, and the sheet address. If nothing is open in any layer, load a file with
   zero proposals and `verdict: settled` with one line of reason.

### C3. Outputs

One proposals file per brand per pass (schema `proposals_schema.json`, extended by K12.3), or a
settled file. Nothing else is written. A pass never approves, rejects, defers, undoes, deploys,
collects or edits a document.

### C4. Stopping conditions

Stop after loading one layer for the named brands. Stop with a note and no file when: the packet
is missing data the proposals would need; the brand is parked; the brand is settled at the
current versions and fingerprint; a note says `wants: a different suggestion` and the pass cannot
find a different one (say so in the pass note; the loader will park it after the round cap).

### C5. What the pass must look for in the survey, in general terms

- 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; ask which it is.
- A value stated on some siblings and silent on others (an age, a strength, a size).
- Any hint list that fired (`audience_fold`, `pack_words`, `format_words`, `age_words`, region,
  cask, skin-type, shade shapes) and whether the register has a precedent under that shape.
- A shop whose wording differs in kind from the others (shorthand against retail names; a code
  where others write words).
- Two languages for one thing; a category descriptor appended; a packaging word: each is its own
  kind, never grouped as "noise".
- Anything the pass would otherwise have called routine and cannot find a precedent for.

---

## Part D. The changes, grouped as the stream builds them

**Before the sweep (the collectors' side).** Undo prunes the product lines a fold minted, and
`backfill prune_lines` clears the 297 empty ones; the parser version moves whenever the kept
payload shape changes, held by a fingerprint test per collector fixture; `collected_name` as an
evidence source; the sweep runbook says `backfill stuck_runs` first and lists which seven sources
a sweep actually reaches.

**The register and the file.** `precedents` table; `precedent`, `precedent_status`, `follows` on
proposals; `precedent` carried onto each decision's detail; `pass_row.survey`, `verdict`,
`fingerprint`; the loader's floors; `precedents list | show | overturn | export`; `pass packet`,
`pass status`, `pass fingerprint`.

**The review itself.** Bulk by precedent group, `all` refused while a lead is open, `NOTE_REQUIRED`
on critical, the batch note on every covered row, the round cap parking to the running list, the
survey at the top of the sheet, unseen words on the bulk confirmation, the fold card stating both
decisions (which row survives, what it is called) with per-direction counts, the merge card
stating which retailers it joins, the collected-name label, the register on the guide tab with
overturn, the index sorted new-kinds-first.

**The documents.** `REVIEW-PROCESS.md` version 6: the layer order, the ladder table, section 4's
thresholds (the count that lets a precedent leave `high`, the unseen-words rule), the terminal
condition, section 3's new fields. `AI-REVIEW-GUIDELINES.md` trimmed to principles with the
survey principle added and the "rewritten before every pass" heading removed. `REVIEW-PASS.md`
installed from Part C. `review-pass.md` rewritten thin.

**The thresholds, adjustable in one place** (REVIEW-PROCESS.md section 4, mirrored as constants
in `attention.py` with a test that they agree): a precedent leaves `high` after **3** consistent
decisions (an overturn or a "something else" resets it); the unseen-words list is shown whenever
it is non-empty and blocks nothing; the round cap is **2** defers before parking. If the first
sweep produces hundreds of criticals, the number that moves is the survey's grouping, not the
gate: a critical is one per *kind*, so hundreds of criticals means hundreds of kinds, which is
information, not noise.

---

## Part E. Where this plan disagrees with the brief

1. **"The gaps are staleness."** Not for the five refused sources (A1). The review must cite the
   collected name for them or it cannot touch 3,847 variants.
2. **"Whether to derive a normalised name on the barcode-rich side."** No (A1).
3. **"The skill grows confident, so attention decays."** Decay is counted from the ledger (decisions
   under a precedent, overturns, unseen words), never from a model's confidence. A model's
   confidence measures its reading of one row; it says nothing about whether rian has seen the
   kind.
4. **"A second checker on the rating."** Not a second model: mechanical floors from a register the
   pass cannot edit (B2).
5. **"Generate the guidelines from the notes."** Generate the precedents; keep the principles by
   hand (B5).
6. **"important."** Stays `high` (B1).
7. **The brief's layer order** gains the cross-divide layer where the simulation said, and the
   survey before all of it.

## Part F. What only rian does, and when

- Approves the plan and starts K12 (it runs in its own worktree; it never touches staging).
- After K12's hand-back: applies the migration and deploys staging; runs `backfill stuck_runs` and
  `backfill prune_lines`; opens a `.logs/runs/` window and runs the sweep on the seven reachable
  sources.
- Then passes: names brands (or ranges), reads each sheet, answers leads, bulk-confirms groups.
  `pass status` says when it is done.
- The timing: this is more than the soft-launch date allows. The brief calls it the one chance to
  get the machinery right before it is pointed at everything; this plan takes that at its word and
  leaves the launch-date call to rian.
