Sada nástrojů pro revize
Review pravidla týmu jako data, auditovaný soudce, mapa textů u každého snímku, kontroly, které umí změřit program, a nálezy ukotvené ke slovům na obrazovce.
Vitrinka čte výstup vašeho runneru
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
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 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,lintaputčtou a publikují soubory pravidel;putnejdřív kontroluje a každé put je nová verze. - Do revize —
vitrinka review brief(MCPreview_brief) vykreslí zapnutá pravidla jako jeden kompaktní blok promptu, s volitelným řádkem--persona. Soudce níže ho používá a/vitrinka:reviewho předá agentovi, který nástěnku reviduje.
Běhy revize
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,allnebo 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.
vitrinka review plan <board> <board> --scope latest
vitrinka review plan --run <id> --cards
vitrinka config reviewer codex # revize tohoto stroje posuzuje Codex sidekickPlá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
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í.
[
{"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
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
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
cy.screenshot pojmenuje soubor; mapu zapište přes task.
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
Stejný průchod DOM jako u Playwright, spuštěný přes execute_script.
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
Zdroj stránky nese nativní hranice v pixelech obrazovky: atributy iOS jsou x y width height, Android má bounds="[x1,y1][x2,y2]".
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
Uvnitř flow se neskriptuje: vypište hierarchii pohledů vedle snímku a jednou ji převeďte.
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.jsonNálezy ukotvené k textu
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.
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.
Volitelné měřicí důkazy
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
vitrinka review lint ./screenshots --viewport 1440x900 --theme dark --locale cs-CZZapíš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
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 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.
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
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.
vitrinka review file <board> screenshots/candidates.json --agent codex --dry-run --json
vitrinka review file <board> screenshots/candidates.json --agent codex --jsonOba 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.
vitrinka review stats --project <project> --since 90dStatistiky 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í.