A screen meets the kit standard when it passes every check below without anyone asking the kit's authors what they meant. That sentence is the whole contract: a check that needs a judgement call from this repo is not a check and does not appear here. The four things a reskin is graded on are the four the owner named together, because they are one judgement about the same screens: the reskin itself (is the kit used as the kit draws things), the categorical layout (is the page composed the way the canon composes pages), visual mess (do neighbours agree, does small text read, is the ramp kept), and the hierarchy of what information and controls are shown (does the prime slot hold the prime thing, is the action set ranked, do counts tell one story).
Two halves. The measured half is bin/kit-standard.mjs, which drives a
live app in both themes at two widths (1280 and 390) and exits non-zero on a fix-severity
finding. The reading half is a list of yes-or-no questions the grading
agent answers per screen with a named piece of evidence, because a script
cannot decide which page archetype a screen is or whether an element earns
its place. Both halves cite the canon line they enforce and the kit source
they hold to, so a finding can be argued with at the rule rather than at the
verdict.
The standard extends two instruments that already exist rather than
repeating them. canon-check reads a diff for the deviation classes nobody
writes a note about (a raw control, a colour literal, a font loaded outside
the slots, a shadowed kit export). The console's presentation-audit.mjs
reads the rendered page for axes 14 to 35 of its presentation standard
(content escaping a container, a machine token where a person's words
belong, an inert control with no reason, a stuck skeleton). This bin reads
the rendered page for the kit's own composition rules. A reskin runs all
three; none of the three sees what another does.
Run it#
From a consumer on 0.3.0 or later, npx kit-standard (every tarball through
0.2.2 omitted bin/). From this repo, the path form:
node packages/ui/bin/kit-standard.mjs --self-testnode packages/ui/bin/kit-standard.mjs \ --base http://127.0.0.1:5109 \ --routes '/dashboard,/jobs,/jobs/*,/files,/files/*,/jobs/new,/admin,/team,/account,/admin/auth,/admin/auth/forge/local,/login' \ --theme-key forge-theme \ --env-file ../versable-forge-v6/.env.local --login-keys FORGE_LOGIN_USER,FORGE_LOGIN_PASSWORD \ --json round-1.json --baseline round-0.json--widths defaults to 1280,390; 390 is the phone width the canon's narrow
rule names, and it replaced 375 in 0.3.0, so a baseline written at 375 does
not match: write a fresh one once. --routes-file <path> reads routes from a
file, one per line, with blank lines and # comments skipped; the app
templates keep theirs in kit.routes.txt.
The second command is the console's. --theme-key is the localStorage key
the app's own boot script reads (forge-theme for the console; the bin also
sets data-theme after every load, so the theme cannot silently stay light
the way a wrong key makes it). --env-file and --login-keys name where the
sign-in credential lives; the values are read from that file and never
printed. Routes under --login-path (default /login) are audited signed
out in a context of their own; everything else signs in first, and a run
that cannot sign in refuses rather than auditing a redirect ten times and
calling it clean. A record route is written as its list plus /*
(/jobs/*): no record URL is knowable ahead of time, so the bin opens the
list, follows its first record link (one level deeper, never /new), and
audits that page. Findings keep the pattern as their route, so a baseline
survives the record changing; the resolved URL rides in the row and the
console line.
Exit 0 means no fix-severity finding, 1 means at least one, 2 means the
run could not happen (no browser, no app, no credential). --json writes
every finding with a stable identity (route, theme, width, check, element
recipe) and --baseline diffs two runs by that identity: what appeared,
what was fixed, what escalated from note to fix. A count cannot see a row
that got worse; the diff can. A baseline row on a route, theme or width the
current run did not visit is reported as not run, never as fixed; the
console's first round-1 run used seven routes against a ten-route baseline
and would otherwise have read three unvisited routes as cleared.
Run --self-test before doubting a finding. It plants one defect of
every class on a blank page and asserts the check fires, then plants the same
defect hidden and asserts it stays quiet; it also fails if any check in the
table below has no fixture that makes it fire. A check nobody has watched
fail is not a check.
What a pass is#
A screen passes the measured half when the bin reports zero fix findings for it in both themes at both widths. Notes are read, not blocked on.
A screen passes the reading half when every question below is answered
"yes" with its evidence named, or "excused" with the kit defect (next
section) that excuses it, or "deviates" with the one-line reason the surface's
own doc records (canon 10-overlays.md §A1: deviation is legitimate and never
free).
A reskin passes when every screen in the route list passes both halves. The
route list is the app's; the console's is the twelve routes above, which are
its presentation audit's own itinerary, the job and file record pages
(discovered from their lists, quoted in the shell so * is not expanded),
and the sign-in page.
The measured half: the checks#
Severity fix holds the exit; note is informational and stays in the
report. Every check names the canon line it enforces (cite), the kit source
it holds to (holds to), and the self-test fixture that proves it fires.
Reskin: the kit's controls used as the kit draws them#
| check | severity | what it measures | cite | holds to |
|---|---|---|---|---|
kit-zero-toolbar-button | fix | an outline neutral button with no icon and a transparent background anywhere in the page body or in an open modal; the kit's zero reads as a placeholder. A coloured outline, a shaded one, an icon button and the pager's own page-number and Previous/Next controls pass | docs/design-language/02-buttons-and-actions.md:47 | button.tsx (variant, shade) |
shade-count-in-group | fix at three, note at two | shaded buttons (a quiet variant resting on a wash of its own colour, alpha between the /5 and /20 rungs) sharing one flex or grid row; a control group carries one shade, two with a stated reason, never three | docs/design-language/02-buttons-and-actions.md:49 | button.tsx (shade), internal/surface.ts (shadeOn) |
uppercase-on-control | fix | computed text-transform: uppercase on a button, a table header or a status pill | docs/design-language/12-typography.md:32 | kicker.tsx (uppercase lives here and nowhere else) |
hand-typed-kicker | note | an uppercase run that is not kicker-shaped (11px tracked or 12px wide): the idiom re-typed by hand | docs/design-language/12-typography.md:23 | kicker.tsx |
kicker-carries-tone | note | a kicker-shaped label in a saturated ink; a coloured label reads as a status | docs/design-language/12-typography.md:35 | kicker.tsx (text-base-content/65, never a tone) |
type-size-off-ramp | note | rendered text at a size outside 11, 12, 14, 16, 18, 20, 24, 30, 36px, aggregated per size | docs/design-language/12-typography.md:14 | the ramp, 12-typography.md §A1 |
button-underline | fix | a button underlined at rest | docs/design-language/12-typography.md:46 | button.tsx (hover feedback is the shade wash) |
native-select | note | a native <select> rendered, except the page-size control inside the kit Pager | docs/design-language/13-forms-and-fields.md:24 | select.tsx (role=listbox); the exempt shape is pager.tsx:118 |
table-outside-card | note | a table with no card or modal box around it | docs/design-language/05-cards-and-surfaces.md:25 | card.tsx, table.tsx |
Categorical layout#
| check | severity | what it measures | cite | holds to |
|---|---|---|---|---|
page-scrolls-sideways | fix | the document's scrollWidth exceeds its clientWidth; never overlap, never a page scroll | docs/design-language/06-tables.md:30 | table.tsx (a minWidth scrolls the table, not the page) |
page-title-count | fix | zero or more than one rendered h1 in main | docs/design-language/12-typography.md:25 | page-title.tsx |
title-after-content | note | a card or table renders before the page title, unless the card holds the title (the auth shell) | docs/design-language/09-page-composition.md:26 | page-title.tsx |
two-crumb-trails | fix | two breadcrumb navs on one page; one location line per page | docs/design-language/08-navigation-and-shell.md:15 | breadcrumbs.tsx (presets: trail, topbar crumb) |
single-crumb-trail | fix | a trail in the body with one crumb; the topbar crumb preset (inside header or the top 64px) is one crumb by design and exempt | docs/design-language/08-navigation-and-shell.md:33 | breadcrumbs.tsx (hideSingle) |
nested-card | fix inside an overlay or inside a card that has its own title, note inside an untitled card | a card inside a modal box, dialog or aside; a card inside a card | docs/design-language/05-cards-and-surfaces.md:22 | card.tsx, modal.tsx |
prose-past-measure | note | a paragraph, list item or definition over 140 characters rendered wider than 76ch of its own font | docs/design-language/09-page-composition.md:18 | the canon's 68ch measure, 09-page-composition.md §A2; no kit primitive |
Visual mess#
| check | severity | what it measures | cite | holds to |
|---|---|---|---|---|
focus-by-border-only | fix | each control in main focused by script and read: a focus that changes only the control's border colour, or changes nothing, with no outline or shadow appearing on the control or the three frames above it; the ring is the only outline and a hue change is not a ring | docs/design-language/13-forms-and-fields.md:34 | theme/fields.css (.field-ring, the frame's :focus-within ring), select.tsx (the control carries it) |
sibling-scale-mismatch | fix | two controls sharing a flex or grid row and overlapping vertically whose heights differ by more than 4px or type sizes by more than 1px; a control inside a kit Input's frame (the password eye) is part of that field, not a sibling | docs/app-patterns/10-recipe-browsable-list.md:172 | button.tsx, input.tsx (the size scales) |
toolbar-height-mismatch | fix | a button, input, combobox trigger or applied badge sharing a flex row whose height differs from the row's modal height by more than 4px; an applied Chip (.badge) is exempt when the row centres its items | docs/design-language/07-toolbars-and-filters.md:16 | src/table/facet-bar.tsx |
text-under-aa | fix for grey ink under 18px, note for a tone or larger text | WCAG contrast of every rendered text run against its composited backdrop, in the theme being run; colours go through a canvas pixel because daisyUI 5 computes to oklch(). Grey ink under 4.5:1 is the ladder broken; a semantic tone on small text in the light theme is the open token call the canon records | docs/design-language/12-typography.md:38 | kicker.tsx (/65 is the small-text floor), theme/colors.css |
A gallery that frames each example in a kit Card marks the frame
kit-example-frame (the playground's ExampleCard does); nested-card and
table-not-bled read that frame as page chrome, so an example that is itself
a card or a table is graded on its own shape. A consumer screen never carries
the class.
A region that shows a banned shape on purpose (a showcase demonstrating what
not to do) opts out of named checks with data-kit-specimen="check-a check-b".
The exemption covers only the checks listed and only what sits inside the
element, so a specimen never hides anything it did not name. A consumer screen
does not carry it. Kit-default defects that wait for a kit change go in a
known list passed with --known <path> ([{check, el, route?, reason}]):
a finding that matches an entry is printed and kept in --json with
known set, and does not fail the run, so a gate stops new defects while the
listed ones wait. An entry that no longer matches anything is reported as
unused, which is the cue to delete it.
Every finding carries a law (L1 to L9, or none) and a kind (visual,
behaviour, copy, a11y, perf, data, safety) from the defect
vocabulary in docs/plan/87-defect-vocabulary.md, in the console line and in
--json, and the run ends with a count by law and kind.
Both themes at both widths are not a check but the run itself: every route is visited four times, and a finding names which theme and width it was measured in. A dark-only sign-off cannot come out of this bin.
Hierarchy of information and controls#
| check | severity | what it measures | cite | holds to |
|---|---|---|---|---|
count-disagrees-with-rows | fix | the kit's Showing a-b of n line beside a table whose rendered row count is not b-a+1 (or not zero when n is zero), skeletons excluded | docs/design-language/07-toolbars-and-filters.md:31 | data-table.tsx:182 |
empty-state-beside-rows | fix | an empty-state sentence (No data, Nothing here, No results, No matches) inside a card that also renders rows | docs/app-patterns/10-recipe-browsable-list.md:144 | list-state.tsx |
id-column-first | note | a table whose first header reads id, uuid or key; the reader's anchor is the name | docs/design-language/06-tables.md:27 | column-factories.tsx (col.link, col.mono) |
row-actions-unranked | note | a table with rows carrying more than three actions in the open, per table; a FilterableCell, a copyCells button, a segmented control's own radio buttons, and anything invisible at rest are not actions a person sees | docs/design-language/02-buttons-and-actions.md:27 | row-menu.tsx |
title-levels | note | more than four distinct heading sizes among h1 to h4 and card titles | docs/design-language/12-typography.md:25 | page-title.tsx, card.tsx, kicker.tsx |
The laws: classes no single component owns#
These check the laws in docs/design-language/00-overview.md ("The eight
laws"). A law check is calibrated, not decreed: a note becomes a fix after a
run over real screens shows it is right more often than wrong. Fit and
baselines start as notes for that reason.
| check | severity | what it measures | cite | holds to |
|---|---|---|---|---|
two-searches | fix | two search inputs (type=search, role=searchbox, or a placeholder or label starting with Search) inside one card, dialog or page; the signal is said once | docs/design-language/00-overview.md:103 | data-table.tsx (Toolbar.Search) |
placeholder-ellipsis | fix | an input or textarea whose placeholder ends in ... or the ellipsis character; a placeholder is a hint, not a prompt to continue | docs/design-language/00-overview.md:105 | input.tsx |
empty-cell-without-na | note | a table body cell with no text, icon or control (not a spanning empty state); a missing value reads N/A | docs/design-language/00-overview.md:105 | column-factories.tsx |
narrow-overflows-viewport | fix | at 480px or narrower: an element whose right edge passes the viewport and sits in no scroller of its own; one finding per outermost offender, ten per page | docs/design-language/00-overview.md:106 | app-shell, table.tsx |
narrow-row-wraps | fix | at 480px or narrower: a header or toolbar row whose children fall onto more than three lines | docs/design-language/00-overview.md:106 | page-title.tsx, facet-bar.tsx |
pointer-not-control | fix | a pointer cursor on an element that is not a link, button, label, summary, table row, tabindex element or an ARIA control, and has no such ancestor; the opposite of clickable-without-pointer | docs/design-language/00-overview.md:107 | button.tsx |
clipped-with-free-space | note | text in an overflow: hidden box that is cut without an ellipsis while its row has 24px or more free; the truncation half is truncate-without-tooltip | docs/design-language/00-overview.md:101 | src/truncated-text |
icon-label-misaligned | note | the vertical centre of an icon and of its label, inside one button, tab or link-button, more than 2px apart | docs/design-language/00-overview.md:104 | button.tsx |
row-controls-misaligned | note | controls in one flex row (field frames, selects, buttons) whose centres sit more than 2px apart, compared only where their heights overlap | docs/ui-categorical/alignment.md, rule 3 | the app's row |
quiet-primary | note | a form's submit button with no fill and no border | docs/design-language/02-buttons-and-actions.md:25 | the app's submit Button |
hover-only-control | note | a button, link or menu item that rests at an effective opacity under 0.05 (its own times every ancestor's); a control must be visible without hover. This is the proposed ninth law, and its cite moves to its own row once the canon pen adds it | docs/design-language/00-overview.md:107 | row-menu.tsx, any opacity-0 group-hover reveal |
hit-target-too-small | note | a button, tab, menu item, checkbox, radio, switch or link smaller than 24 by 24px (the WCAG 2.2 floor); a checkbox counts its label, and a link inside running text is exempt | docs/design-language/00-overview.md:107 | button.tsx, input.tsx |
The kit's patterns, where a screen departs from a proven composite#
| check | severity | what it measures | cite | holds to |
|---|---|---|---|---|
dropzone-in-bordered-card | fix | a dashed dropzone edge inside a card that draws its own border; the compact pattern makes the dashed edge the card | docs/design-language/05-cards-and-surfaces.md:53 | dropzone.tsx, the compact-dropzone pattern |
table-not-bled | fix | a table inside a card body inset more than 4px from the body's edge, a SchemaForm rows table included; rows meet the card edges | docs/design-language/05-cards-and-surfaces.md:25 | table.tsx:45 (BLEED) |
empty-table-no-state | fix | a table with a header and an empty body and no skeleton; a kit Table renders its empty state in one spanning cell, a bare empty body says nothing | docs/design-language/04-loading-and-states.md:21 | table.tsx (the empty slot), list-state.tsx |
auth-card-count | note | a signed-out route (login, signup, forgot, reset) with more or fewer than one card | docs/design-language/05-cards-and-surfaces.md:33 | card.tsx, the auth-card-shell pattern |
auth-card-shape | note | that card not centred, wider than 480px, or missing an input or a submit | docs/design-language/05-cards-and-surfaces.md:33 | card.tsx, the auth-card-shell pattern |
search-not-first | note | a search input inside a card with a control rendered to its left on the same row; the search slot leads the toolbar | docs/design-language/07-toolbars-and-filters.md:18 | data-table.tsx (Toolbar.Search leads) |
pager-needless | fix | a kit pager whose Previous and Next are both disabled: one page, and the bar still renders; the pager hides when there is nothing to page | docs/design-language/00-overview.md:102 | pager.tsx, data-table.tsx (the pager hides at one page) |
tab-without-icon | fix | a tab strip (role=tablist) with a tab that has no icon; one finding per strip, naming the bare tabs | docs/design-language/00-overview.md:104 | tabs.tsx (item.icon) |
status-with-copy | note | a status pill whose own word reappears in a sentence beside it (a pill reading Complete next to "This job is complete"); the pill is the whole statement | docs/design-language/00-overview.md:103 | status-pill.tsx |
truncate-without-tooltip | fix | text cut with an ellipsis that shows no tooltip on hover. The probe dispatches the pointer events a mouse would, waits a third of a second, and looks for a role=tooltip or a title attribute; it checks the first 15 truncated runs on a page | docs/design-language/00-overview.md:101 | the kit truncation tooltips (status-pill.tsx, chip.tsx, table cells) |
clickable-without-pointer | fix | an enabled button, link, tab, radio, switch, menu item or option whose computed cursor is not the pointer. A disabled or aria-disabled control, a link on the browser's own default and anything hidden pass; one finding per distinct element class | docs/design-language/00-overview.md:107 | theme/components.css (the base-layer pointer rule) |
Copy: the words an app puts on screen#
The kit asks for short, plain words. These three checks are notes for now.
They measure length and punctuation only. Whether a sentence is formal and
plain is the reading half's call. A placeholder ending in an ellipsis is
placeholder-ellipsis, above, and is a fix.
| check | severity | what it measures | cite | holds to |
|---|---|---|---|---|
helper-text-long | note | helper text under a field over about 90 characters | this section | input.tsx (bottom) |
helper-text-semicolon | note | helper text with a semicolon in it | this section | input.tsx (bottom) |
tab-label-long | note | a tab label of more than three words; a bare count is not a word | this section | tabs.tsx (item label) |
Kit defects the standard excuses#
A standard that requires a component with a known source defect fails on contact, so each defect below names the check or question it excuses and what a consumer may do until the kit fixes it. A consumer is not graded down for the workaround; it is graded down for a workaround with no row behind it. Rows are in the versable-builder store; the first three are gcp row #492, confirmed in the 0.2.2 source.
| defect | where | excuses | until fixed, a consumer may |
|---|---|---|---|
formatRelativeTime has no future branch, so any future time renders "just now". Fixed in 0.3.0 (test/kit-defects-492.test.tsx); the excuse holds for a pin below 0.3.0 | timestamp.tsx | reading question 10 for any expiry, schedule or due column | pass absolute on that column, as the console's Invitations table does, until the pin moves |
ConfirmModal hardcodes its title icon to Trash and only for preset="delete". Fixed in 0.3.0 with an Icon prop; the excuse holds for a pin below 0.3.0 | confirm-modal.tsx | reading question 4's icon clause for a non-delete confirm | ship the confirm without a title icon; do not hand-roll a raw confirm, modal.md bans it |
FacetBar treats the caller's first option as the all-sentinel, so a single facet shows an unapplied filter as applied. Fixed in 0.3.0 (allValue is required on a single facet); the excuse holds for a pin below 0.3.0 | facet-bar.tsx | reading question 5 for a single-select facet | list an explicit { value: "all", label: "Any time" } first, as the console's History facet does |
The FacetBar count line and the Select's "nothing to choose" row were text-base-content/50, under AA in the light theme, as were the SchemaForm review's "Not set" placeholders at /45. Fixed in 0.3.0, all at the /65 floor; the excuse holds for a pin below 0.3.0 | facet-bar.tsx, select.tsx, schema-form/review.tsx, schema-form/widgets.tsx | text-under-aa on those runs | nothing; the finding is the kit's and does not count against the screen until the pin moves |
Input renders its top caption outside the <label>, so the visible caption is not the control's accessible name | input.tsx | reading question 11's caption clause | nothing; the kit's, tracked in canon 02-buttons-and-actions.md §A9 |
The compact-dropzone pattern's own example types text-[15px] for its headline | the pattern page's source | type-size-off-ramp at 15px on a dropzone headline | copy the pattern as shipped; the ramp question is the kit's to settle |
When one of these is fixed, its row closes and the excuse leaves this table in the same commit; a consumer on a pin that predates the fix keeps the excuse until it upgrades.
The reading half: yes or no, per screen#
Each question is answered once per screen with the evidence line it names. "Yes" needs the evidence; "no" is a finding; "excused" names a row from the table above; "deviates" names the sentence in the surface's own doc that records why. There is no fifth answer. Screenshots are taken with the app's own tool at 1280 and 375 in both themes and the answer cites the file path; a screenshot read is described before the question is answered, because a prepared question turns looking into a slower DOM assertion.
Categorical layout
- Which archetype is this screen, and does the DOM agree? A reading page
caps its width and scrolls the whole page; a work surface goes full width
and its table's card owns the scroll. Evidence: the archetype named, and
scrollHeightagainstclientHeightonmainand on the table's card. A work surface whosemainscrolls is a no.docs/design-language/09-page-composition.md:13. - Is the composition order the canon's? Title, then stat tiles, then
cards, sections and tables, with nothing the page drew itself as chrome
(no page-owned crumb, sidebar or topbar). Evidence: the order of
main's children by name.docs/design-language/09-page-composition.md:26,docs/design-language/08-navigation-and-shell.md:15. - Does every instance of this page type render all its zones? A record page, a list page, an auth card: name the zones and confirm each instance lays them out even when a zone is empty. Evidence: the zone list and one screenshot per instance. The console's presentation standard, axis 22.
Hierarchy of information and controls
- Has the action set been identified and ranked? Every action on the
surface listed as (icon, title, details, action, colour); two or three in
the open as icon-only controls with a rich tooltip; the rest in one menu;
anything destructive coloured. Evidence: the inventory, and the menu
opened in a screenshot.
docs/design-language/02-buttons-and-actions.md:27; the console's presentation standard, axis 26. The icon clause on a non-delete confirm is excused, see above. - Does every visible element name the decision it changes? For each
element, one clause. "None, but we know it" is a no, unless the surface
is an admin inspect surface whose audience differs. Evidence: the list.
This is the class the owner's rejected rebuild failed first
(
contract/v6/ui-regression-classes.mdin the operations workspace, class A). A single-select facet showing an unapplied value as applied is excused, see above. - Is width assigned from hierarchy, not from what the grid permits? The smallest table on the page does not sit in the widest slot; a card that gains nothing from room does not get it. Evidence: each card or table's width beside a one-line reason. The same doc, class B.
- Is the count line honest? "N of M" only while a filter is active,
the plain count otherwise, and the count and the rows come from one
source. Evidence: the line's text in the filtered and unfiltered states.
docs/design-language/07-toolbars-and-filters.md:31. - Does the identifying column hold the human identifier? The name cell is the anchor; a machine id sits at the bottom of the action menu, copyable. Evidence: the first column's header and one cell. The console's presentation standard, axis 27.
Visual mess
- Was every floating layer opened over content, in both themes? Each
dropdown, popover and menu opened while it overlaps rows; its painted
surface has an opaque background and a border or shadow, and
elementFromPointat its centre lands inside it. Evidence: the measured values per layer.docs/ui-categorical/patterns.md:15in this repo, class 1. - Is mono only for identifiers, and colour on text only for status or
intent? Evidence: every mono run and every toned run listed with what
it is.
docs/design-language/12-typography.md:19,docs/design-language/12-typography.md:35. A future timestamp column is excused, see above. - Is every interactive thing a real control with a name? No
divwith anonClick; every icon-only control has an accessible name; a caption is associated with its field. Evidence: the list of icon-only controls with their names.docs/design-language/02-buttons-and-actions.md:41. The Input caption clause is excused, see above. - Do the loading, empty, filtered-empty and failed states each say what
happened, without collapsing into one another? Evidence: one
screenshot per state, induced.
docs/design-language/04-loading-and-states.md:21,docs/app-patterns/10-recipe-browsable-list.md:144.
Reskin
- Does each screen shape follow its pattern? For every kit pattern the
inventory names for this shape (the compact dropzone, the auth card
shell, the bulk selection bar, the confirm-and-report action, the job
title composite), the screen matches the pattern page or its doc records
the deviation. Evidence: the pattern route and the screenshot side by
side.
docs/KIT-INVENTORY.md:118. - Does every mutation settle into feedback owned by something that
survives it? A toast or inline state on trigger, success and error,
never a component that unmounts on success. Evidence: the handler's
three paths, and the toast observed within five seconds.
docs/design-language/02-buttons-and-actions.md:35. - Does each table speak one selection language? A checkbox column and
a click tint never disagree. Evidence: click a row body, read the tint,
the checkbox and the bulk bar together.
docs/ui-categorical/patterns.md:175, class 8. - Was every behaviour of the old page written as a row before the reskin, and does each row carry a result? A rebuild that regenerates every decision is pinned to nothing. Evidence: the parity ledger's path and its empty cells, if any. This is the class that let a ruled plan pass its own checks while the owner rejected the page.
Grading rounds#
Two, and the limit is hard. A round is the console lane running the bin over
its route list with --json, answering the sixteen questions per screen, and
handing both files back with what moved since the last round and what proved
it. This repo reads the handback and answers only two things: whether a
finding it disputes is a defect in the instrument (in which case the check
gets a fixture and the fix in the same commit, never a workaround), and
whether a question was answered with its evidence. It does not re-grade the
screens by eye; that is the verdict lane this seat was redefined out of.
If the second round fails, the third round is not a grade. It is this file rewritten more precisely at the points the two rounds show were open to reading, and the bin given fixtures for whatever the rounds proved it could not see. A standard that needs its author in the loop to apply is the dispatch tax the fleet charter names, and the remedy is writing, not grading.
A disputed finding is argued at the element recipe, not the verdict: the
handback names the check, the el string from the JSON and why the kit
shape it flagged is a kit shape. Five of the first run's eleven classes were
the instrument being wrong about kit shapes (the topbar crumb, the password
eye inside an Input, a FilterableCell counted as a row action, the
base-content ink read as saturated, a separator glyph read as small text),
and every one became a quiet fixture in --self-test. That is the loop: the
instrument is fixed, never defended.
What the measured half cannot see#
A hover state, an opened overlay, a wrong sentence, a missing icon, which archetype a page should be, whether an element earns its place, what a colour means. A clean run is the floor. The reading half exists for exactly these, and the questions are binary so that the answer never needs this repo.
The self-test covers the checks, not the app. It proves each check fires on
a planted defect and stays quiet on a hidden one; it does not prove the
console has none. The round-0 baseline for the console, measured 2026-09-08
against the running dev console after calibration, is the first real reading:
35 fix findings, every one small grey text under AA in the light theme at
the /45, /50, /55 and /60 rungs the ladder removed, and 72 notes across
seven shapes. That file is the baseline the first grading round diffs against.