Генерация артикула резолвила 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
63 lines
3.2 KiB
Markdown
63 lines
3.2 KiB
Markdown
# 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`.
|