# Stream K1: one term per concept, everywhere (the consistency pass)
(written 2026-09-16 Wed, from the accepted plan §1 W2 and §8: rian overruled the after-Cannes timing; the renames run now, alone, before the parallel wave, so every later stream builds on the final names)

**Runs:** alone, the night of Wed 16 Sep, after K0's hand-back, on the refactor branch in the
worktree. Nothing else runs in the tree: a rename touches every file the other streams own.

## Goal
Every concept has one word, identical in the database, the code, the API, the labels and prose
(rian, 16 Sep: *"I want the technical language in the database to match what I'm being told as a
human in the labeling"*). One rename-only migration, every identifier and label moved, every test
green, no behaviour changed. The glossary's "code says" column shrinks to nothing.

## The rename map (plan W2; this is the whole of it, nothing else changes)

| Today | Becomes | Where |
|---|---|---|
| table `products`, model `Product`, FK `product_id` (on listings, awards, reverifications, merge candidates, the pin) | `product_variants`, `ProductVariant`, `variant_id` | models, alembic, every service, router, script, test, the SPA's types and field names |
| `products.line_id`, `ProductLine.key`'s callers `line_key`, `line_for`, `lines.py` | `product_line_id`; `product_line_key`, `product_line_for`; `product_lines.py` | the same |
| `brands.canonical_id`, `product_lines.canonical_id`, `resolve_alias` | `alias_of_id`; `resolve_alias` stays (it is the word) | models, keying, merges, desk, tests |
| `house` in identifiers (`house=`, `Maps.house()`, `house_words`, `_house_counts`, `house_name`, `house_slug`, `house_canonical`) | `brand` / `brand_of()` / `brand_words` / `_brand_counts` / `brand_name`, `brand_slug`, `alias_of` | services, listings table registry, tests |
| `merge_candidates`, `MergeCandidate`, the legacy `product_id`/`candidate_id` pair on it | `suggestions`, `Suggestion`; the legacy pair dropped (every row has `left_id`/`right_id` since Stream M) | models, alembic, suggest, desk, session, routers, SPA |
| `decision` values `merged` / `kept_apart` / `superseded` / `withdrawn` on suggestions; the route `reject`; the button "Keep apart" | `same` / `separate` / `superseded` / `withdrawn`; route `keep-separate`; buttons **Confirm same** / **Keep separate**; the `/collectors` tab **Suggestions** | a data backfill for the two values (idempotent), the routers, the SPA |
| `product_merges`, `ProductMerge` | `merges`, `Merge` | models, alembic, merges.py, tests |
| `variation_aliases`, `VariationAlias`, `attributes.variation`, `attributes.variation_kind`, `VARIATION_KINDS`, `VARIATION_RULES`, `VARIATION_BEHAVIOR`, `determinant` / `descriptor` | `attribute_aliases`, `AttributeAlias`; the value stays under `attributes` but keyed by its kind (`attributes.concentration`, `attributes.color`) with no separate `variation` key; `ATTRIBUTE_KINDS`, `ATTRIBUTE_RULES`; the display setting `picked` / `shown` / `fact` | K1 renames the identifiers and the JSON key; K2 builds the registry on top |
| `overrides`, `Override`, `collected_value` | **left for K2**, which rebuilds the table as `decisions` (plan W15); K1 renames only the identifier `collected_value` where it is read, to `rule_value`, through the shim K2 will replace | services, tests |
| `size_ml`, `size_value`, `size_unit` | `quantity_ml`, and the v3 pair `quantity_stated_value` / `quantity_stated_unit` | models, alembic, every reader, the SPA (`size_ml` in `schema.ts` and the format helpers) |
| `locations`, `Location`, `location_id`, `location_*` API fields, `is_catalogue_only` | `shops`, `Shop`, `shop_id`, `shop_*`; `is_catalogue_only` stays | models, alembic, ingest, catalog queries, hours store, the SPA's airport and price types |
| labels: "House", "Collected brand", "Suggested match", "Merge" (tab), "Keep apart", "Products" (tab, listing variants) | "Brand", (dropped when `products.brand` goes, K2), "Suggested match" stays, "Suggestions", "Keep separate", "Product variants" | listings table registry, desk, collectors page |

What K1 does **not** rename: `sources` (a different concept, stays), `listings`, `brands`,
`product_lines` (already right), `raw_records`, `price_observations`, the public URL `/products/`
(not prose; it shows product lines), the API paths under `/api/` (renamed only where a field name
changes; a path is a contract the SPA and the typed client regenerate together).

## Tasks, in order (ids on /plan; commit prefix `K1:`; one commit per row of the map, tests green at each)
1. **K1.1 The migration.** One Alembic revision, rename-only (`op.rename_table`, `op.alter_column`
   new_column_name, index and constraint renames, the FK renames), chained on the current head;
   rehearse `upgrade head`, `downgrade -1`, `upgrade head` on a fresh copy of the newest dump on
   `dfp-devdb`; `alembic heads` prints one head. The two suggestion decision values change by a
   backfill (`app.cli backfill suggestion_decisions`, idempotent), never inside the migration.
2. **K1.2 Models and services.** Rename in `app/models/`, then every service, in the map's order.
   Delegate the mechanical edits to a subagent with the exact map and `pytest` as the acceptance;
   verify every diff yourself. `lines.py` becomes `product_lines.py`; keep a one-line import shim
   for one commit only, then remove it (no shim survives K1).
3. **K1.3 Routers, the typed client and the SPA.** Field names in `schemas.py` follow the map
   (`location_*` to `shop_*`, `size_ml` to `quantity_ml`, `products` counts to `product_variants`);
   `main/check.sh` regenerates `web/src/api/schema.ts`; the SPA's components and format helpers
   follow; labels per the map; the `/collectors` tab words.
4. **K1.4 Tests, scripts and docs.** Every test, `main/scripts/*.py`, `deploy/`, `main/docs/*.md`
   prose and the docmap-generated blocks (`docmap.py --write`), `VOCABULARY.md`'s last column
   emptied for every row K1 covered, `DATA-MODEL.md`'s prose. A test that greps `main/app` and
   `main/web/src` for the retired identifiers (`house`, `canonical_id`, `merge_candidates`,
   `kept_apart`, `size_ml`, `Location\b`, `location_id`, `\bProduct\b` as the variant, `VARIATION_`,
   `determinant`, `descriptor`) and fails on any survivor outside `alembic/versions/`, the
   CHANGELOG and the planning docs.
5. **K1.5 The after-deploy line and the hand-back.** `RUNBOOK.md`: the migration id and the one
   backfill; the CHANGELOG line ("the review area and the database say the same words").

## Owns
Everything under `main/` and `deploy/` for the duration; this is why K1 runs alone.

## Must not touch
`.app.env`, `.env`, the live database, `import/` data files other than `progress.json` through the
script. K1 changes no behaviour: a test that changes its expected value (not its identifier) is a
bug in K1.

## Rehearsal and acceptance
- The migration up, down, up on a fresh copy of the newest nightly dump (`dfp-devdb`,
  `DATABASE_URL=postgresql+psycopg://dfp:dfp@127.0.0.1:5433/dfp_k1`), then `rederive --check` on the
  copy printing zero changed keys (a rename changes no identity), then the suggestion-values
  backfill twice (second run zero).
- `main/check.sh` green; the survivor test green; `docs-check.sh --strict` no FAIL.
- Report in the handoff: the migration id, the count of identifiers renamed per row of the map, the
  rehearsal's zero-change proof.

## Rollback
`downgrade -1` reverses every rename; the branch's previous commit is the code rollback. Nothing
else changes.

## Hand-back
Handoff (≤25 lines), `/checkpoint`, and the line "K1 green and ready; K2 may start; the wave builds
on the new names". The migration id goes into the launch chain (K7).
