# 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`.