Генерация артикула резолвила 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
88 lines
2.2 KiB
Go
88 lines
2.2 KiB
Go
package article
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
||
)
|
||
|
||
type Group string
|
||
|
||
const (
|
||
GroupCPU Group = "CPU"
|
||
GroupMEM Group = "MEM"
|
||
GroupGPU Group = "GPU"
|
||
GroupDISK Group = "DISK"
|
||
GroupNET Group = "NET"
|
||
GroupPSU Group = "PSU"
|
||
)
|
||
|
||
// GroupForLotCategory maps pricelist lot_category codes into article groups.
|
||
// Unknown/unrelated categories return ok=false.
|
||
func GroupForLotCategory(cat string) (group Group, ok bool) {
|
||
c := strings.ToUpper(strings.TrimSpace(cat))
|
||
switch c {
|
||
case "CPU":
|
||
return GroupCPU, true
|
||
case "MEM":
|
||
return GroupMEM, true
|
||
case "GPU":
|
||
return GroupGPU, true
|
||
case "M2", "SSD", "HDD", "EDSFF", "HHHL":
|
||
return GroupDISK, true
|
||
case "NIC", "HCA", "DPU":
|
||
return GroupNET, true
|
||
case "HBA":
|
||
return GroupNET, true
|
||
case "PSU", "PS":
|
||
return GroupPSU, true
|
||
default:
|
||
return "", false
|
||
}
|
||
}
|
||
|
||
// ResolveLotCategories returns lot_category for each lotName, read from the component
|
||
// universe (latest active world ∪ estimate pricelists — the same source the configurator,
|
||
// BOM and pricing tab use, see bible-local/decisions/2026-07-24-component-universe-world-union.md).
|
||
// It must NOT be scoped to a single pricelist: a world-only LOT is a legitimate cart member
|
||
// and still carries a real lot_category in the world pricelist. LOTs present in neither
|
||
// pricelist are omitted — callers treat a missing key as "no category" and skip that lot.
|
||
func ResolveLotCategories(local *localdb.LocalDB, lotNames []string) (map[string]string, error) {
|
||
if local == nil {
|
||
return nil, fmt.Errorf("local db is nil")
|
||
}
|
||
cats, err := local.GetLocalComponentCategoriesByLotNames(lotNames)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for lot, cat := range cats {
|
||
cats[lot] = strings.TrimSpace(cat)
|
||
}
|
||
return cats, nil
|
||
}
|
||
|
||
// NormalizeServerModel produces a stable article segment for the server model.
|
||
func NormalizeServerModel(model string) string {
|
||
trimmed := strings.TrimSpace(model)
|
||
if trimmed == "" {
|
||
return ""
|
||
}
|
||
upper := strings.ToUpper(trimmed)
|
||
var b strings.Builder
|
||
for _, r := range upper {
|
||
if r >= 'A' && r <= 'Z' {
|
||
b.WriteRune(r)
|
||
continue
|
||
}
|
||
if r >= '0' && r <= '9' {
|
||
b.WriteRune(r)
|
||
continue
|
||
}
|
||
if r == '.' {
|
||
b.WriteRune(r)
|
||
}
|
||
}
|
||
return b.String()
|
||
}
|