# Sada nástrojů pro revize (https://docs.vitrinka.ai/cs/qa/review-toolkit)



## Vitrinka čte výstup vašeho runneru [#law]

Sada nástrojů pro revize pracuje s tím, co váš testovací harness už tak zanechává: se snímky obrazovky a s volitelnou mapou textů vedle každého z nich. Vitrinka nikdy nespouští prohlížeč, neřídí zařízení a neprovádí OCR. Vše, co pravidlo potřebuje, je soubor, který vaše sada v Playwright, Cypress, Selenium, Appium nebo Maestro zapíše na několik řádků, a každé pravidlo je zde zveřejněno, aby si ho tým mohl místo toho implementovat ve vlastní sadě.

## Review brief [#brief]

Pravidla, podle kterých se reviduje, jsou data týmu na serveru, ne prompt, který někdo pokaždé přepisuje. Každé pravidlo je jeden markdown dokument: frontmatter `id`, `family`, `check` (`mechanical`, `partial` nebo `judgment`), `severity`, volitelně `platform` a `enabled`, pak věta pravidla a seznam `## Verify`. Pravidla se podle id skládají ze tří vrstev — vestavěná rubrika, vydestilovaná ze skutečných anotací z revizí, pak pravidla workspace, pak přepisy projektu.

* **V prohlížeči** — **Nastavení → Workspace → Review** a [záložka Review](/cs/pm/projects#review) projektu vypíšou každé výsledné pravidlo s vrstvou, ze které pochází, přepínačem zapnutí, editorem a návratem k vrstvě pod ním. Měnit pravidla může admin; při uložení se zkontrolují, zverzují a zapíšou do auditu.
* **Z terminálu** — `vitrinka review rules list`, `get`, `lint` a `put` čtou a publikují soubory pravidel; `put` nejdřív kontroluje a každé put je nová verze.
* **Do revize** — `vitrinka review brief` (MCP `review_brief`) vykreslí zapnutá pravidla jako jeden kompaktní blok promptu, s volitelným řádkem `--persona`. Soudce níže ho používá a `/vitrinka:review` ho předá agentovi, který nástěnku reviduje.

## Běhy revize [#runs]

Revize přes mnoho nástěnek nemá pokaždé číst každý screenshot. `/vitrinka:review` začíná **plánem revize**, který připraví server: jedno volání nad nástěnkami rozhodne, co je třeba posoudit, a otevře **běh revize**, který si vede záznam o tom, co se skutečně vidělo.

* **Rozsah** — výchozí je poslední průchod snímání každé cesty. Nástěnky s několika průchody (nebo s mnoha obrazovkami) odpoví cenou každé možnosti — obrazovky, obrazovky k posouzení, archy — a plán počká, než se rozsah zvolí: `latest`, `all` nebo pojmenované sekce.
* **Jen to, co se liší** — prázdné, skeleton a nedekódovatelné snímky i snímky, kterým runner selhal, jsou **mezery ve snímání**: nikdy nález, nikdy úspěch. Bajtově shodné obrazovky se posoudí jednou. Téměř shodná obrazovka stejného zařízení a motivu se posoudí výřezem jen těch pixelů, které se liší od zástupce skupiny. Obrazovka posouzená v dřívějším dokončeném běhu podle stejné rubriky se **použije znovu**; úprava kteréhokoli pravidla vynutí posouzení všeho.
* **Důkaz, že recenzent vidí** — první arch běhu nese v pixelech krátký kód, nikdy jako text. Recenzent ho zopakuje dřív, než smí číst arch 2 nebo založit nález, takže hostitel, kterému obrázky nedorazí, skončí po jednom obrázku místo hlášení „žádné nálezy“.
* **Účtenka pro každou kartu** — každá obrazovka v rozsahu skončí jako prohlédnutá, zastoupená (výřezem nebo shodným dvojčetem), znovu použitá, mezera ve snímání, nebo blokovaná. Nálezy už otevřené na stejné verzi obrazovky jsou vypsané u její dlaždice a jejich opětovné založení se odmítne jako duplikát.

```sh
vitrinka review plan <board> <board> --scope latest
vitrinka review plan --run <id> --cards
vitrinka config reviewer codex   # revize tohoto stroje posuzuje Codex sidekick
```

Plán uvádí svou obrazovou cenu vedle spočtené základny — každá obrazovka v rozsahu na přehledových arších po 12 — takže úspora je číslo. Přes MCP jsou tytéž dveře `review_plan` a archy běhu se čtou přes `get_card_image {run, sheet}`.

## Mapa textů: `boxes.json` [#boxes]

Ke každému obrázku zapište sourozence pojmenovaného `<image filename>.boxes.json`, včetně přípony obrázku: `login.png.boxes.json`. Soubor je pole JSON; každá položka nese `text`, `x`, `y`, `w`, `h` a volitelné `role`. Souřadnice jsou pixely zdrojového obrázku s počátkem vlevo nahoře. Šířka a výška musí být kladné a každá souřadnice konečná. Záporné počátky jsou povoleny, aby lint mohl hlásit přetečení.

```json
[
  {"text":"Continue","x":24,"y":320,"w":144,"h":40,"role":"button"},
  {"text":"Email","x":24,"y":200,"w":60,"h":20,"role":"label"}
]
```

Zachyťte text přesně tak, jak je zobrazen, v pořadí zachycení, včetně duplicit. Limity jsou 2 MiB a 20 000 položek; neznámá pole a chybějící povinná pole jsou odmítnuta.

Mapa cestuje se snímkem všude, kam cestuje snímek: `vitrinka qa run publish` posílá sourozence vedle původní cesty ke snímku každého reportéru, `vitrinka board capture --boxes <path>` přijímá explicitní mapu (a sourozence adoptuje automaticky) a push sady zahrnuje sourozence vedle jejích obrázků. Server každou mapu zmrazí do revize karty, takže opětovné publikování sady nikdy nepohne kotvami dřívější revize.

## Zápis mapy z vašeho runneru [#runners]

Webové snímky se pořizují v poměru pixelů zařízení, proto CSS obdélníky vynásobte `devicePixelRatio`; nativní nástroje už hlásí pixely obrazovky. Každý úryvek sbírá viditelné textové uzly a zapisuje sourozence vedle snímku, který popisuje.

### Playwright [#playwright]

```ts
const boxes = await page.evaluate(() => {
  const dpr = window.devicePixelRatio || 1;
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
  const out: { text: string; x: number; y: number; w: number; h: number; role?: string }[] = [];
  for (let n = walker.nextNode(); n; n = walker.nextNode()) {
    const text = n.textContent?.trim();
    if (!text) continue;
    const range = document.createRange();
    range.selectNodeContents(n);
    const r = range.getBoundingClientRect();
    if (r.width <= 0 || r.height <= 0) continue;
    const role = (n.parentElement?.closest("[role]") as HTMLElement | null)?.getAttribute("role") ?? undefined;
    out.push({ text, x: r.x * dpr, y: r.y * dpr, w: r.width * dpr, h: r.height * dpr, role });
  }
  return out;
});
await page.screenshot({ path: "login.png" });
fs.writeFileSync("login.png.boxes.json", JSON.stringify(boxes));
```

### Cypress [#cypress]

`cy.screenshot` pojmenuje soubor; mapu zapište přes task.

```js
cy.screenshot("login");
cy.document().then((doc) => {
  const dpr = doc.defaultView.devicePixelRatio || 1;
  const boxes = [...doc.querySelectorAll("button, a, label, h1, h2, h3, p, span, input")]
    .map((el) => ({ el, r: el.getBoundingClientRect(), text: (el.innerText || el.value || "").trim() }))
    .filter(({ r, text }) => text && r.width > 0 && r.height > 0)
    .map(({ el, r, text }) => ({ text, x: r.x * dpr, y: r.y * dpr, w: r.width * dpr, h: r.height * dpr, role: el.getAttribute("role") || undefined }));
  cy.task("writeFile", { path: "cypress/screenshots/login.png.boxes.json", body: JSON.stringify(boxes) });
});
```

### Selenium [#selenium]

Stejný průchod DOM jako u Playwright, spuštěný přes `execute_script`.

```python
boxes = driver.execute_script("""
  const dpr = window.devicePixelRatio || 1, out = [];
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
  for (let n = walker.nextNode(); n; n = walker.nextNode()) {
    const text = (n.textContent || '').trim(); if (!text) continue;
    const range = document.createRange(); range.selectNodeContents(n);
    const r = range.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) continue;
    out.push({text, x: r.x * dpr, y: r.y * dpr, w: r.width * dpr, h: r.height * dpr});
  }
  return out;
""")
driver.save_screenshot("login.png")
open("login.png.boxes.json", "w").write(json.dumps(boxes))
```

### Appium [#appium]

Zdroj stránky nese nativní hranice v pixelech obrazovky: atributy iOS jsou `x y width height`, Android má `bounds="[x1,y1][x2,y2]"`.

```python
import re, xml.etree.ElementTree as ET
root = ET.fromstring(driver.page_source)
boxes = []
for el in root.iter():
    text = (el.get("label") or el.get("text") or el.get("content-desc") or "").strip()
    if not text: continue
    if el.get("bounds"):
        x1, y1, x2, y2 = map(int, re.findall(r"\d+", el.get("bounds")))
        rect = dict(x=x1, y=y1, w=x2 - x1, h=y2 - y1)
    else:
        rect = dict(x=float(el.get("x")), y=float(el.get("y")), w=float(el.get("width")), h=float(el.get("height")))
    if rect["w"] > 0 and rect["h"] > 0:
        boxes.append(dict(text=text, **rect, role=el.tag))
driver.get_screenshot_as_file("home.png")
open("home.png.boxes.json", "w").write(json.dumps(boxes))
```

### Maestro [#maestro]

Uvnitř flow se neskriptuje: vypište hierarchii pohledů vedle snímku a jednou ji převeďte.

```sh
maestro screenshot home.png
maestro hierarchy > home.hierarchy.json
jq '[.. | objects | select(.attributes.text? and .attributes.bounds?)
     | (.attributes.bounds | capture("\\[(?<x1>\\d+),(?<y1>\\d+)\\]\\[(?<x2>\\d+),(?<y2>\\d+)\\]")) as $b
     | {text: .attributes.text, x: ($b.x1|tonumber), y: ($b.y1|tonumber),
        w: (($b.x2|tonumber) - ($b.x1|tonumber)), h: (($b.y2|tonumber) - ($b.y1|tonumber))}
     | select(.w > 0 and .h > 0)]' home.hierarchy.json > home.png.boxes.json
```

## Nálezy ukotvené k textu [#anchors]

Agent zakládající nález přes `annotate` už nehádá pixely. Oblast může být `{ "text": "Continue" }`, nebo `{ "text": "Continue", "nth": 1 }` pro druhý výskyt. Shoda je přesná a rozlišuje velikost písmen, proti mapě deklarované verze karty; nejednoznačný nebo chybějící text odpoví `unresolved` a nikdy se místo něj nevybere blízký popisek. Pixelové oblasti `{x, y, w, h}` a normalizované oblasti `{nx, ny, nw, nh}` zůstávají alternativami; nekombinujte je.

`category` (id revizního pravidla nebo starší kategorie) a `severity` jsou u každého agentního nálezu povinné. S `dryRun: true` dávka rozliší každou kotvu a vrátí obdélník plus URL výřezu připnutého k verzi, aniž by cokoli zapsala, takže se špatná kotva odhalí dřív, než přistane na nástěnce.

<Callout type="info">
  `GET /api/v1/boards/{slug}/cards/{id}/boxes.json?version=N` čte mapu přesně jedné revize. Běžné nahrání bez sourozence má pixely, ale žádnou mapu, a textová kotva na něm zůstává záměrně `unresolved`.
</Callout>

## Volitelné měřicí důkazy [#metrics]

Položka může nést `metrics`. Vynechejte, co runner nezná; nikdy hodnoty nevymýšlejte. Zachyťte jen porovnatelné prvky, které pravidlo potřebuje, protože přiřadit každému textovému uzlu rodiče nebo skupinu popisuje DOM, ne zamýšlený vztah rozvržení.

| Pole                       | Význam                                                                                       |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| `group`                    | Porovnatelní sourozenci, kteří mají mít stejné svislé rozestupy                              |
| `parent: {x,y,w,h}`        | Kontejner, vůči němuž má mít obsah vyvážené boční odsazení                                   |
| `id`, `labelFor`           | Zachycená identita cíle a výslovný vztah popisku ke vstupu                                   |
| `textInset`                | Vodorovné odsazení textu vstupu v pixelech zdroje                                            |
| `primary`                  | Známá klasifikace primární akce, true nebo false                                             |
| `textTransform`            | Vyhodnocená transformace, například `none` nebo `uppercase`                                  |
| `textWidth`                | Neoříznutá šířka primárního popisku v pixelech zdroje; šířka položky je jeho dostupná plocha |
| `foreground`, `background` | Neprůhledné vyhodnocené barvy `#RRGGBB`                                                      |
| `backgroundSource`         | `literal` jen tehdy, když inspekce zdroje prokáže doslovnou barvu                            |

## Jedenáct mechanických kontrol [#lint]

```sh
vitrinka review lint ./screenshots --viewport 1440x900 --theme dark --locale cs-CZ
```

Zapíše `screenshots/candidates.json`; `--output` zvolí jinou cestu. Podporované obrázky jsou PNG, JPEG, WebP a GIF do 64 milionů pixelů. Viewport se ve výchozím stavu rovná rozměrům obrázku; téma a locale musí popisovat zachycení.

| Kontrola                   | Požadované důkazy a test                                                                                      | Pravidlo / výchozí závažnost |
| -------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| `duplicate-text`           | Přesně opakovaný text; kandidát u druhého výskytu                                                             | redundant / medium           |
| `sibling-gap-variance`     | Nejméně tři seskupení sourozenci s levými hranami do 1 px; rozdíl největší a nejmenší svislé mezery přes 4 px | spacing / low                |
| `side-padding`             | Explicitní rodič; rozdíl levého a pravého odsazení přes 4 px                                                  | spacing / low                |
| `label-input-alignment`    | Vztah popisku, id vstupu a odsazení textu; rozdíl levých hran přes 1 px                                       | spacing / low                |
| `uppercase-transform`      | Zachycená transformace rovna `uppercase`                                                                      | copy / low                   |
| `cs-ascii-words`           | České locale; slovo je v omezeném slovníku chybějící diakritiky                                               | copy / medium                |
| `primary-button-count`     | Klasifikace primárních tlačítek; více než jedno primární                                                      | hierarchy / medium           |
| `text-contrast`            | Vyhodnocené barvy a čtyři neprůhledné vzorky rohů obrázku shodné s pozadím; kontrast pod 4.5:1                | hierarchy / high             |
| `outside-viewport`         | Obdélník textu přesahuje deklarovaný viewport                                                                 | layout-width / medium        |
| `primary-label-truncation` | Neoříznutá šířka primárního popisku přesahuje dostupnou šířku o více než 1 px                                 | layout-width / medium        |
| `dark-literal-white`       | Tmavé téma a zdrojem potvrzené doslovně bílé pozadí                                                           | dark-theme / medium          |

Kandidát je měření k ověření člověkem, nikdy automatický požadavek na opravu. Bez mapy běží jen heuristika světlých oblastí v tmavém tématu; každá další kontrola je zaznamenána ve `skipped` s důvodem. Prázdný seznam kandidátů nikdy nedokazuje, že přeskočené kontroly prošly.

## Soudce a audit [#judge]

Co program změřit neumí, jde k soudci: serverovému průchodu, který každou zachycenou obrazovku posoudí podle pravidel briefu a vrátí verzované výsledky podložené důkazy. Spustíte ho přes **run review** na nástěnce (nabídka ⋯ nebo review pohled panelu Annotations), příkazem `vitrinka review judge <board>` nebo přes `/vitrinka:review <board> eve`. Běží na [AI poskytovateli](/cs/pm/eve#settings) organizace.

* **Připnutý** — každá úloha si připne verze karet a verzi rubriky, které posuzovala, takže výsledek vždy říká, na co se díval.
* **Auditovaný** — každý navržený nález a během kalibrace i vzorek průchodů nezávisle zkontroluje audit; auditor vidí důkaz a tvrzení, nikdy model, který je vytvořil. Chybějící důkaz zůstává **Needs evidence**, nikdy pass.
* **Zakládáte vy** — výsledky se ukazují v review pohledu panelu Annotations, seskupené podle pravidla, každý s výřezem důkazu. **File finding** ho odloží na nástěnku; **Dismiss** ho zahodí; hodnocení užitečnosti (valuable, minor, not useful) se drží odděleně od vlastního posouzení AI. Nic se za vás nezaloží, nevyřeší ani nezamítne.
* **Ověřený po opravě** — **Verify a fix** zkontroluje auditem čerstvý snímek téže obrazovky proti nálezu.

```sh
vitrinka review judge <board> --rules spacing,copy --instructions "check the empty states"
vitrinka review judge <board> --status
vitrinka review judge export <job> --json
```

`--section` nebo `--journey` průchod zúží, `--set` posoudí publikovaný set a `export` zapíše posudky jako JSON, který projdete a označíte `keep` před založením tak, jak ukazuje další sekce.

## Zrevidovat, založit a učit se [#file]

Projděte každého kandidáta, nastavte `keep: true` jen u nálezů, které stojí za založení, a doplňte `cardId` a `cardVersion` karty obrázku z dokumentu nástěnky. CLI nikdy neodvozuje revizi z názvu souboru.

```sh
vitrinka review file <board> screenshots/candidates.json --agent codex --dry-run --json
vitrinka review file <board> screenshots/candidates.json --agent codex --json
```

Oba příkazy používají dávkové dveře annotate. Zkušební běh rozliší stejné kotvy a vrátí URL výřezu pro každou připravenou položku; výřezy si prohlédněte a pak zakládejte. Založené nálezy jsou nativní anotace ve stavu staged, které čekají na Accept nebo Dismiss člověka v panelu Annotations na nástěnce.

```sh
vitrinka review stats --project <project> --since 90d
```

Statistiky počítají založené, přijaté, zamítnuté a dosud čekající nálezy podle pravidla a agenta v rámci vaší viditelnosti nástěnek. Míra zamítnutí je podíl zamítnutých k součtu přijatých a zamítnutých, takže čekající nálezy ji nikdy nevylepší, a je null, dokud nikdo nerozhodl. Tato míra je signál driftu: pravidlo, které lidé stále zamítají, je pravidlo k přepsání.
