# Decision: the component universe is `world` ∪ `estimate`, not `estimate` alone **Date:** 2026-07-24 **Status:** active ## Context Every component read path in `internal/localdb/components.go` was scoped to the latest active `estimate` pricelist. That pricelist is therefore what defined the set of LOTs the configurator considers to exist, and the frontend used it as its validation list (`_bomLotValid` in `index.html`, fed by `/api/components`). That silently broke BOM mappings. The resolver matches a vendor partnumber against the partnumber book, which knows nothing about pricelists, so it legitimately returns LOTs the estimate pricelist does not carry. The frontend then displayed such a row as mapped — `_getRowBaseLot` does not validate `resolved_lot` — and `applyBOMToEstimate` pushed the LOT into the cart, but `_getRowCanonicalLotMappings` filtered it out through `_bomLotValid`, so the mapping vanished on save. Result: the LOT sat in the cart while its BOM row showed "н/д", and the pricing tab listed it at the bottom as an orphan with an empty vendor PN. Prices for such LOTs were already available — see [2026-07-10-world-pricelist-fallback.md](2026-07-10-world-pricelist-fallback.md) — only membership was missing. ## Decision `componentUniverse()` in `internal/localdb/components.go` is the single source of the LOT set, and every component read path goes through it: `ListComponents`, `SearchLocalComponents`, `SearchLocalComponentsByCategory`, `GetLocalComponent`, `GetLocalComponentCategories`, `GetLocalComponentCategoriesByLotNames`, `CountComponents`. - The universe is the latest active `world` pricelist — the widest list available — plus the latest active `estimate` pricelist as a safety net. - Deduplication is on `UPPER(lot_name)`. The estimate row wins a collision, so its category, description and `price_quality` stay authoritative and nothing changes for LOTs that were already visible. - A world-only row reports `price_quality = 0`, rendering at the red end of the shared scale in `web/static/price-quality.js`. This is the **one** place QF sets that field instead of only reading it (contrast `bible-local/03-database.md`), and it deliberately overrides whatever the external pricing tool wrote on the world row. - The helper always returns a derived table (`(?) AS c`), so callers apply their own `Select`/`Count`/`Where` without knowing which pricelists exist. - Either pricelist may be absent; only both missing is an error. On the frontend, `_bomLotPersistable` additionally lets a LOT with `resolution_source === 'book'` be persisted into `lot_mappings[]` even when it is in neither pricelist. Hand-typed LOTs stay validated against the universe. ## Consequences - The configurator's pickers, category list and search now surface world-only LOTs. They are selectable and appear in the reddest quality colour. - `CountComponents` — the "is there data" check — counts the union. - A LOT present in neither pricelist (nothing to price it with) still resolves from the book and now survives a save, instead of being dropped without a message. - Covered by `internal/localdb/component_universe_test.go`: union and dedup, estimate winning a collision, forced quality 0, and each single-source / empty case.