Генерация артикула резолвила lot_category из одного прайслиста конфигурации (GetLocalLotCategoriesByServerPricelistID), поэтому world-only LOT (напр. GPU_NV_RTX_PRO_6000D_SERVER_84GB_PCIE, есть только в world) молча выпадал из артикула. Теперь категории берутся через GetLocalComponentCategoriesByLotNames (тот же world ∪ estimate, что и весь конфигуратор). BuildOptions.ServerPricelist убран; preview-article принимает pricelist_id, но игнорирует. Нераспознанные токены больше не пишутся как UNK: в артикул идёт lot_category как плейсхолдер (4xGPU, 2xCPU), сегмент помечается Recognized=false, добавляется warning с именем LOT. Конфигуратор подсвечивает такие сегменты (amber) и выводит список предупреждений; сохранение/обновление/откат логируют WARN. Без каталога вендоров в репо детектируется только структурный сбой формы имени. parseGPUModel: принимает суффикс-букву в номере модели (6000D → RTX6000D), раньше терял её и схлопывал до RTX_84GB. ADL bible-local/decisions/2026-09-01-article-category-from-component-universe.md, 2026-09-01-article-degraded-token-visibility.md; раздел «Article generation» в 02-architecture.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RAhfF4P1ySRZ67yyUUeVw2
3.2 KiB
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:
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.BuildOptionsno longer hasServerPricelist.POST /api/configs/preview-articleand the create/update paths still acceptpricelist_idbut 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 thelot_name— that produces the article text, it does not categorize. One parser bug was fixed alongside:parseGPUModelrequired the model-number token to be all digits (isNumeric), soRTX_PRO_6000Dlost itsDand collapsed toRTX_84GB— indistinguishable from a realRTXcard and different from its siblingRTX_PRO_6000→RTX6000_96GB. It now accepts a digit-first alphanumeric token (isModelNumber), givingRTX6000D_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/estimatepricelist recategorizes a LOT — acceptable, and consistent with how the configurator's category tabs already behave. GetLocalLotCategoriesByServerPricelistIDstill exists and is still used byservices/rental.goandservices/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_WorldOnlyLotininternal/article/categories_test.go.