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
+28 -1
View File
@@ -2537,13 +2537,40 @@ async function previewArticle() {
}
const data = await resp.json();
currentArticle = data.article || '';
el.textContent = currentArticle ? ('Артикул: ' + currentArticle) : 'Артикул: —';
renderArticleDisplay(el, currentArticle, data.segments, data.warnings);
} catch(e) {
currentArticle = '';
el.textContent = 'Артикул: —';
}
}
// renderArticleDisplay shows the article, colouring any segment the generator could
// not fully parse (Recognized === false — its token is a category placeholder, not a
// real spec) and listing the warnings that name the offending LOTs.
function renderArticleDisplay(el, article, segments, warnings) {
if (!article) { el.textContent = 'Артикул: —'; return; }
let body;
if (Array.isArray(segments) && segments.length) {
body = segments.map(s => {
const t = escapeHtml(s.text);
return s.recognized === false
? '<span class="text-amber-700 bg-amber-100 rounded px-0.5" title="не распознано — записана категория">' + t + '</span>'
: t;
}).join('-');
} else {
body = escapeHtml(article);
}
let html = 'Артикул: ' + body;
if (Array.isArray(warnings) && warnings.length) {
const list = warnings.filter(w => w !== 'compressed')
.map(w => '<li>' + escapeHtml(w) + '</li>').join('');
if (list) {
html += '<ul class="mt-1 text-xs text-amber-700 list-disc list-inside font-sans">' + list + '</ul>';
}
}
el.innerHTML = html;
}
function getCurrentArticle() {
return currentArticle || '';
}