fix: артикул — категории из component universe + видимость нераспознанных токенов

Генерация артикула резолвила 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
This commit is contained in:
Mikhail Chusavitin
2026-09-01 09:01:56 +03:00
co-authored by Claude Sonnet 5
parent ecef030699
commit d7d4ea74b6
11 changed files with 512 additions and 153 deletions
@@ -0,0 +1,66 @@
# Decision: unparsed article tokens degrade to a visible category placeholder, never "UNK"
**Date:** 2026-09-01
**Status:** active
## Context
`internal/article` builds each segment by parsing a spec token out of the `lot_name`
(CPU model, memory size, GPU model, disk capacity, port speed, wattage). When a
`lot_name` did not match the expected shape the generator failed silently or
misleadingly:
- `parseCPUModel` / `parseGPUModel` fell back to `normalizeModelToken` or a literal
`"UNK"` with **no signal** — and a partial parse (`RTX_PRO_6000D``RTX_84GB`)
produced a plausible but wrong token indistinguishable from a real result;
- `NET` / `PSU` emitted `UNKNET` / `UNKPSU`;
- `MEM` / `DISK` dropped the segment or the capacity and emitted only a terse
`mem_unknown` / `disk_unknown` warning;
- `BuildResult.Warnings` existed but **nothing consumed it**: the `preview-article`
frontend ignored the field and the create/update paths discarded it.
So a new naming shape (a vendor renames a card family, moves memory ahead of the
model, glues an architecture onto the token) would silently produce a wrong or
truncated article and no one would notice.
A vendor/model catalog in the repo to validate tokens against was rejected — it
violates `bible/rules/patterns/no-hardcoded-vendors` and needs constant upkeep.
Detection is therefore limited to **structural** failure (the name doesn't fit
`{GROUP}_{VENDOR}_{MODEL}[_{SPEC}…]`); semantic drift where a wrong token still
parses cleanly cannot be caught automatically and is out of scope.
## Decision
1. `"UNK"` / `"UNKNET"` / `"UNKPSU"` are gone. When a spec token can't be parsed the
article carries the LOT's `lot_category` as the token (`4xGPU`, `2xCPU`,
`768G+MEM`, …) via `placeholderToken`.
2. Each `build*Segment` returns a `segmentResult{value, degraded, warnings}`. Every
degraded segment produces a warning that **names the offending `lot_name`** and
states which category was written instead.
3. `BuildResult` gains `Segments []ResultSegment{Group, Text, Recognized}`.
`Recognized == false` marks a segment whose token is a placeholder.
4. `parseCPUModel` / `parseGPUModel` return `(token, ok)`; `ok == false` on the
last-ditch fallback path.
5. Consumers surface it:
- `POST /api/configs/preview-article` returns `segments` and `warnings`; the
configurator's article line renders each not-`recognized` segment highlighted
(amber) and lists the warnings beneath it (`renderArticleDisplay` in
`web/templates/index.html`);
- create / update / rollback log `WARN "article generation degraded"` with the
`server_model`, article and warnings.
6. No list of real `lot_name`s or model names is committed to the repo — not as a
fixture, not as a golden table. Tests use synthetic names only.
## Consequences
- An unrecognised LOT is always visible in the UI (highlighted token + warning) and
in server logs — never a silent drop or a bare `UNK`.
- The stored `article` string still contains only the placeholder token; the
`recognized` flags are not persisted (regenerated on next save / preview).
- `compressArticle` now returns `[]namedSeg` (the caller re-joins) so the degraded
flags survive compression.
- Structural detection only: a wrong-but-well-formed token (e.g. a future `GTX`
where `RTX` was expected) still passes. Closing that needs a validation source,
deferred pending a server-side structured-attribute contract.
- Covered by `TestBuild_UnparseableModel_CategoryPlaceholder` and
`TestParseGPUModel_VendorLetterSuffix`.