---
id: w1/m8/t002
title: "Collapse the two code copies onto the catalog: operator tierResources, store tiers/normalizeTier"
worker: worker1
status: done
estimate: 30m
depends_on: [w1/m8/t001]
---

## Objective

Delete both existing hardcoded ladders and point them at the t001 catalog, so a ladder change is one reviewed file edit.

## Context

- `lego/operator/internal/controller/app_controller.go:154-180` — `tierResources` map + `resourcesForTier(tier)`. Keep the exact semantics: known tier ⇒ requests==limits (Guaranteed), **empty/unknown tier ⇒ no constraints** (best-effort) so bare-CR/kubectl apps keep working. The operator reads only cpu/memory — money never enters the mechanism layer (the catalog has none anyway).
- `lego/backend/internal/store/api.go:322-341` — `var tiers`, `tierFree`, `normalizeTier`. Preserve the empty-string default (`"" → free`, now the catalog's default id) and the error message listing valid names (now from the catalog).
- Note: `normalizeTier` gates both app `tier` and tenant `plan` today — that stays as-is; whether tenant plans deserve their own ladder is a later question, not this milestone's.
- Both modules already import `lego/types`; no new dependency edges.

## Steps

1. `resourcesForTier` becomes a catalog lookup; delete the map; adjust its tests to assert against the catalog.
2. `normalizeTier` validates against catalog ids; delete the local list/const.
3. `make test` (from `lego/operator/`) + backend `go test ./...` green.

## Files

- `lego/operator/internal/controller/app_controller.go`
- `lego/backend/internal/store/api.go`

## Acceptance criteria

- [ ] `grep -rn "pro-ultra" lego/operator lego/backend --include='*.go'` finds no hardcoded ladder outside tests.
- [ ] Empty/unknown `spec.tier` still yields a best-effort pod; store empty tier still defaults to `free` (both regression-tested).
- [ ] `make test` + backend tests green.

## Out of scope

- Public API `plan` (t003); changing the Guaranteed (requests==limits) policy; tenant-plan ladder redesign.
