# Decision: article generation reads lot_category from the component universe, not one pricelist **Date:** 2026-09-01 **Status:** active ## Context `internal/article/generator.go` classifies each cart LOT into an article segment (CPU / MEM / GPU / DISK / NET / PSU) by its `lot_category`. That category was resolved by `ResolveLotCategories` → `GetLocalLotCategoriesByServerPricelistID`, which queries exactly **one** pricelist — the configuration's pinned `pricelist_id` (an `estimate` pricelist). A segment builder skips any LOT whose category is unknown, so a LOT absent from that one pricelist vanished from the article with no warning. `GPU_NV_RTX_PRO_6000D_SERVER_84GB_PCIE` exposed this: it exists only in `world` pricelists (where it correctly carries `lot_category = GPU`) and in no `estimate` pricelist. It was priced (world fallback), exported, and shown in the cart, but dropped from the article. This is the same class of bug as [2026-07-24-component-universe-world-union.md](2026-07-24-component-universe-world-union.md): the cart's LOT set is `world` ∪ `estimate`, and no read path may silently drop a LOT that only lives in `world`. Article generation was the one component-reading path still scoped to a single pricelist. Deriving the category from the `lot_name` prefix was rejected — it is forbidden by the pricelist-contract rules (`02-architecture.md`, `bible/rules/patterns/`): category comes only from real synced pricelist columns. ## Decision `ResolveLotCategories(local, lotNames)` now calls `LocalDB.GetLocalComponentCategoriesByLotNames`, which reads the component universe (`componentUniverse()` — latest active `world` ∪ `estimate`, estimate wins a collision). The server-pricelist parameter is gone. - `article.BuildOptions` no longer has `ServerPricelist`. - `POST /api/configs/preview-article` and the create/update paths still accept `pricelist_id` but ignore it for article purposes. - The `lot_name`-token parsing inside a segment (CPU model, memory size, GPU model, disk capacity, port speed, wattage) still parses the token from the `lot_name` — that produces the article text, it does not categorize. One parser bug was fixed alongside: `parseGPUModel` required the model-number token to be all digits (`isNumeric`), so `RTX_PRO_6000D` lost its `D` and collapsed to `RTX_84GB` — indistinguishable from a real `RTX` card and different from its sibling `RTX_PRO_6000` → `RTX6000_96GB`. It now accepts a digit-first alphanumeric token (`isModelNumber`), giving `RTX6000D_84GB`. ## Consequences - A world-only LOT lands in the article with its real category. Article generation no longer depends on which pricelist a LOT was added to. - The article can shift if the latest active `world`/`estimate` pricelist recategorizes a LOT — acceptable, and consistent with how the configurator's category tabs already behave. - `GetLocalLotCategoriesByServerPricelistID` still exists and is still used by `services/rental.go` and `services/export.go`. Those paths have the same single-pricelist blind spot for world-only LOTs; migrating them is out of scope here but should follow the same direction. - Covered by `TestResolveLotCategories_WorldOnlyLot` in `internal/article/categories_test.go`.