Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/audit-kit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ jobs:
- run: bunx playwright install --with-deps chromium
- run: bun run test:audit
- run: bun run test:audit:self
- run: bun run test:audit:realsites
- run: bun run test:showcase:self
- run: bun run test:audit:parity

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions consumer-reference/agent-native/v2/search-embeddings.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@
"vector": "/gIA+AEAAv////8BAv8B/gYB/wIH/AD8BAf9/Abm/gMBAPwE/gQC/f0FA/4A/v399AL/Av4JAgcOAgr9AQLuCv8F/QT4AP799QQD/gL8//kBBAIDBgH9/AX7AfoF/fb9AAQAL/wABwMC/wUB/gD9/vv8A/7+BQT7+wT4/PoAAgUL+/8D+QIBA/z+AgICBQEF9hf/Cf3+/An9AfwF///2AgX8+wkA9f8IBAEC/AED+QH5/wUFDPr4/wb+AAALBgUB/wf+AwIE/v4FBvn7/wAA/QX98wj9AwT7CAkFBfr5+9T//AH+////+/wF/fv//f4H/Pb5/wb+9wYGCwH//wEI+vgDA/z5/PoBBAH8Avv8+vgE/gEACPwC+/sC+gH+CPn8/v8BAf7/B/76BQD/APr+AAYHBP4BBfgF//8FAfsEB90CBAz5AgP4/v37BAP++wEP/Aj8//0Z+v36BP4CBQn6DAgBBAABAQEFAfYBB/4AAvwA/AD+/gn6/Qb///8D/goF"
},
"expression/anti-patterns.md": {
"text_sha256": "0eb1da315cc5123cfcdfeee264df43e812ebefbdfbf407cd7411d4fd67998535",
"vector": "9wP/BAT//wX9+/8HBAP9AAEBAAIKA/z6Ag37+f7n/f4GAAICAQgAA/4GAwMLAAL69gMA/gIC/wUDAgAHAAfyCQED+/wBA/78+wUEAwD8BwD9A/4B/wQCBAj6BfgC/v38+QL/LPj/AQYC/gX//f/7AAD5/fkBBQn4/gIB//4HCQYGAgX4+/8G//oF/P//BQf++w39Bvr3AgX++vEG/gL8Bf38/A0A7gAE/gT9AfwA+gH6+v0CAPf8/v/8+v4C//v+/wH/AwAA+/wGBP37BQAA/AQE8wL3/wD//wEABP0H99UG/AP69wYD/QQLBvb8AQEIAfr9AAQC+QMEEAABAQIF//wB///9AAD8Av/4Av36+PwCAPz8CgIF+fkC/AMBBvkG/P8DCAEDDwL2Bv//AfwGBQACAwEF/foM/gIJ+wEH+dsBAw79/wT2Afr8DQL6BAEM+wQA/gEZAAD9BvkBAwbyCgT//wD/Af/9AfYDAfn/Bf//Bfn9/gMBBAIB/gAABAoD"
"text_sha256": "51a55b9003b4e5bfb593dae4d1fcfd2e865689b6f7f49ae46f1b32370281831f",
"vector": "9gP/BAX//wX//AAGBAP8AAEBAAILA/76Aw37+P7m/v4GAAICAggBA/8FAgMKAAP69wQA/gMD/wUDAgAGAQfzCQED+vsAA/77+wUFAwD8BwD9Av4B/wQBBAj5BPcD/v78+QL/LfgAAQYC/gT//f/6AAH5/fkCBQn4/gEB/v4HCgYGAgT4/P8G//sF/P7/BAf/+g79Bvr3AwX/+/EG/QL7Bf77/AwB7wAE/wT9AfwB+gH6+/4C//f7/v/8+/0CAPv+/wH/A/8A+/sGBP37BQAA+wUE8gH3AP//AAIABP0G99UE/QP79wYD/gQKB/X8AgIIAfr9/wMB+QMDEQAAAQIF/vwB//79AQD8Av/4Av36+f0DAP37CQEG+fkC/QMBBvkH+/8CCAIEDwL2BwD+APsGBQECAwEF/foL/gII/AEH+tsCAw79/wT2Afr8DAL5BAEM+wUB/gEZAAD8BfgBAgXyCgX//wD/Af/9AfUEAfn/BP//Bvn9/gMBBQEC/f//BAsC"
},
"expression/brand-studies.md": {
"text_sha256": "15aab2071034096201d0b4f84253e2e02cbe86c8773686e4aa6901bc5359e99f",
Expand Down Expand Up @@ -650,8 +650,8 @@
"vector": "9wID+vwDBf3//vsBAf79AfwA//8C+//7/Qr+/QDn+AQBA/kCAAP/AgYF///++QD99gD/AwMGAwQDBQP+AwHvDQQDBPn+//kB/AAJ+AEFBfz9BgEB/AT/BQX9+/QD/P7+AwH8Lf/8/AEBAQT8+wn9/gMAAv/+/gX6Av4E+gD9AwMA+gj/+wMAAwT+Av8DAwn8/RT9/f0AAQT+A/wFBwb8//v7+g3/9f0F/wT+AwP//gLvAQEFB/f2AAIAAQABB/3+BQX/BP/7//4DAvf4//35AA4A8QED/v36AwP/B/r/+df9+QL/+gQA//gIAPwE/wED/Pr+/Af89/4CCv0RAgMKAfv/DQL7BAEAAvz4/v/9+/0G/gL+A/cB/PwD/AIBB/YBAAMBBAH5Bv3+BQj+BPgA/wEEBwH8BvsJ+AH/+/QBBt0ECwf3AgL1+wD6BwAC/QYSAAL9/wYZAAX+A/4FAwT4CAQD/wMI/gT9Cfv5CP4G/P8A//wB/wr+A/r+BAkDCgAK"
},
"quality/audit/README.md": {
"text_sha256": "29876ebc08ec38bb1264ea0a5201859d87edf04cd0f73943ea48987d90a2a247",
"vector": "+gcA/P0CAgX9AAgA//74/P4BCQQH//z6BBH//PfnAvcA/f8D/gYF/AD/AfoB+gL6//7+/wUC/AMGCwj/AwPuB/4H+vb/BAL++wQE+wYB+wT6Agf+AgkAAwX8AvsCAPgDAAD5Lv8I/wQFAAMBBfwABgD0//7/A/8AAAkK/P8BAwUKA///+v0D+/0BAfz4B/3+9hH7CP36AQL/+/kBAgT/BPr4/w738QIE/wH+Af77/Pn+AgD6Afr/+wcEAPwEBP8A/QsD/PkG+P4FCgIEAv8B+QL/9gP8BQIC/AL+AvX7/tj3AQb/+wQG+/wNAvj+/wYEAPwCAQP4+hAEEPoAAv7+A/cG/f8F/Pz5Bvvy//32/AMBAwT7/wH/+vr6/AMCBQUE9gAHAv3+Cf70CQP7+PoC/wUEAAAI+v8F/QH/AfwEA+L9/gz7AwAA/Pv/CgEBBPoI/gf7AQcWAQH8CQEF/Qb9BAoCA/sC/gb8A/7+Bfv3/wIDAfsB/v8DBAYC/vsJBgcC"
"text_sha256": "617d252a27cb89aecbdd4e33906b37a8c9df97ab8e72f8bf22d2408eb7fcdb77",
"vector": "+ggA/f4CAgX+AAcBAP/5+/0ACQUH/f36BRH+/PfmAvj//f8E/wYE/AABAfoC+wL5///+/wQC/QIGCwf/AwPuB/4G+vb/AwL++gMF+gYC/AT6Awj+AggBAwT8A/sCAPgDAAD4L/8I/wUFAAMBBf0ABgD0AP//A/4AAQkK/f8DBAUKA///+v0D+/4AAfz6CP3+9xD6CP75AQL/+/kCAgT/BPn4/wz38gMF/gH9AP76+/n9AgH6APoA+wcEAfsDBP8A/goD/PkF9/0GCwMEAv8C+QMA9gT8BQAC/AL/A/b7/9f3AQYA/AMG+/wOA/f9/wYDAfwCAAP5+hEEEPoAAv/+A/cH/f8E/f36Bvzz//72/AMBBAT8/wL++vn6/QIDBQQF9QEHAv7+Cf/1CQP7+PsC/gUE/wAI+gAF/QAAAfwEA+H9/wv8A/8B/PwACgL/BPsI/gb7AQcWAQD8CQAE/Ab+AwoCAvsB/Qb7A/79Bfv2/wIDAvoC//4EBAYD/vsJBQYD"
},
"quality/audit/ideal-state.md": {
"text_sha256": "6333b991b08ec4bae755f15887af9008509cb964cf675336a5d33e245e4ef159",
Expand Down
16 changes: 8 additions & 8 deletions consumer-reference/policies/lifecycle-a2a-extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@
"scripts/test-lifecycle-disposition.mjs",
"scripts/validate-lifecycle-disposition.mjs"
],
"scanned_file_count": 654,
"scanned_paths_content_id": "sha256:8bf145d59289cd2f21c991401352f8830e2ce2968c0aff65418747a02fbd1768"
"scanned_file_count": 657,
"scanned_paths_content_id": "sha256:345ca11fe456c0af4c8388dc92d25be8fb86ec13baaeaddca572324400184374"
},
"implementation": {
"path": "scripts/agent-native/v2/extensions/a2a-projection.mjs",
Expand Down Expand Up @@ -145,10 +145,10 @@
"role": "repository_reference",
"lines": {
"start": 1,
"end": 388
"end": 390
},
"file_sha256": "b6c02f2a376d4a405899aa8698052e7faa40e238b9563a2f82a8403eec2979e5",
"content_id": "sha256:b6c02f2a376d4a405899aa8698052e7faa40e238b9563a2f82a8403eec2979e5",
"file_sha256": "a7b2ee8e5cd90da22c965ffa33c3affc4fb6839ff63e5179553d375f42bd1d99",
"content_id": "sha256:a7b2ee8e5cd90da22c965ffa33c3affc4fb6839ff63e5179553d375f42bd1d99",
"reference_lines": [
252,
268
Expand Down Expand Up @@ -432,10 +432,10 @@
"role": "installed_package_exposure",
"lines": {
"start": 1,
"end": 388
"end": 390
},
"file_sha256": "b6c02f2a376d4a405899aa8698052e7faa40e238b9563a2f82a8403eec2979e5",
"content_id": "sha256:b6c02f2a376d4a405899aa8698052e7faa40e238b9563a2f82a8403eec2979e5"
"file_sha256": "a7b2ee8e5cd90da22c965ffa33c3affc4fb6839ff63e5179553d375f42bd1d99",
"content_id": "sha256:a7b2ee8e5cd90da22c965ffa33c3affc4fb6839ff63e5179553d375f42bd1d99"
},
"forwarding": {
"path": "scripts/agent-native/a2a-projection.mjs",
Expand Down
16 changes: 8 additions & 8 deletions consumer-reference/policies/lifecycle-ag-ui-extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@
"scripts/test-lifecycle-disposition.mjs",
"scripts/validate-lifecycle-disposition.mjs"
],
"scanned_file_count": 654,
"scanned_paths_content_id": "sha256:8bf145d59289cd2f21c991401352f8830e2ce2968c0aff65418747a02fbd1768"
"scanned_file_count": 657,
"scanned_paths_content_id": "sha256:345ca11fe456c0af4c8388dc92d25be8fb86ec13baaeaddca572324400184374"
},
"implementation": {
"path": "scripts/agent-native/v2/extensions/agui-projection.mjs",
Expand Down Expand Up @@ -203,10 +203,10 @@
"role": "repository_reference",
"lines": {
"start": 1,
"end": 388
"end": 390
},
"file_sha256": "b6c02f2a376d4a405899aa8698052e7faa40e238b9563a2f82a8403eec2979e5",
"content_id": "sha256:b6c02f2a376d4a405899aa8698052e7faa40e238b9563a2f82a8403eec2979e5",
"file_sha256": "a7b2ee8e5cd90da22c965ffa33c3affc4fb6839ff63e5179553d375f42bd1d99",
"content_id": "sha256:a7b2ee8e5cd90da22c965ffa33c3affc4fb6839ff63e5179553d375f42bd1d99",
"reference_lines": [
253,
269
Expand Down Expand Up @@ -479,10 +479,10 @@
"role": "installed_package_exposure",
"lines": {
"start": 1,
"end": 388
"end": 390
},
"file_sha256": "b6c02f2a376d4a405899aa8698052e7faa40e238b9563a2f82a8403eec2979e5",
"content_id": "sha256:b6c02f2a376d4a405899aa8698052e7faa40e238b9563a2f82a8403eec2979e5"
"file_sha256": "a7b2ee8e5cd90da22c965ffa33c3affc4fb6839ff63e5179553d375f42bd1d99",
"content_id": "sha256:a7b2ee8e5cd90da22c965ffa33c3affc4fb6839ff63e5179553d375f42bd1d99"
},
"forwarding": {
"path": "scripts/agent-native/agui-projection.mjs",
Expand Down
30 changes: 24 additions & 6 deletions expression/anti-patterns.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@
"body_size": "The character-weighted modal computed font-size, rounded to the nearest pixel, over all visible elements' own text nodes (the measured-benchmarks.md console method).",
"interactive": "a[href], button, summary, [role=button], [role=link], [role=tab], input[type=button], input[type=submit], excluding elements with the disabled attribute or aria-disabled=true."
},
"profiles": {
"detection": "stylegallery audit check resolves the profile before checking: an explicit --profile wins; otherwise app when the page declares role=application or shows 3 or more form controls on at most 2 screens with links under 60% of the text, content when links carry at least 70% of the rendered text, or at least 60% with at most 3 content images, or when the page has no content imagery and at least 1500 characters of text, and landing otherwise. The resolved profile, its source, and the signals are reported.",
"landing": {
"description": "A marketing, brand, or product landing page: the finished-page surface the 49 measured originals represent.",
"relaxed": [],
"rationale": "Every rule in this catalog was calibrated on landing and brand homepages (expression/measured-benchmarks.md), so all machine rules are errors."
},
"app": {
"description": "A product or tool surface: dashboards, editors, search and booking forms, signed-in views.",
"relaxed": ["audit.empty-screen", "audit.link-transition", "slop.headline-only-gradient-screen", "slop.empty-screen", "slop.gradient-area-excess", "slop.unfinished-surfaces", "slop.static-interactive", "slop.sluggish-state-change", "slop.oversized-display", "slop.repeated-card-triplet"],
"rationale": "Richness floors, display scale, surface finishing, and motion density were measured on landing pages; application surfaces trade them for density and stability, so these rules become warnings. Accessibility checks, honest actions, emoji icons, unanchored violet gradients, and soft shadows stay errors."
},
"content": {
"description": "A reading or index surface: articles, documentation, government and news pages, link directories.",
"relaxed": ["audit.empty-screen", "audit.link-transition", "slop.headline-only-gradient-screen", "slop.empty-screen", "slop.gradient-area-excess", "slop.unfinished-surfaces", "slop.static-interactive", "slop.sluggish-state-change", "slop.oversized-display", "slop.repeated-card-triplet", "slop.soft-shadow-excess"],
"rationale": "Text-led pages are finished by their text, not by imagery: GOV.UK screens of 8.5% and 9.9% text and Hacker News links without hover transitions are correct for their purpose, yet missed the landing floors in the 2026-10-19 real-site run. Landing-only rules become warnings; accessibility checks and honest actions stay errors."
}
},
"entries": [
{
"id": "slop.headline-only-gradient-screen",
Expand Down Expand Up @@ -142,10 +160,10 @@
"detectable": "machine",
"severity": "error",
"detection": {
"scope": "Every visible interactive element (measurement_context.interactive) at 1440x900 with pointer: fine.",
"scope": "Every visible interactive element (measurement_context.interactive) at 1440x900 with pointer: fine. Elements that are zero-size or at most 1px, outside the document, clipped by an overflow ancestor or a clip, inside [aria-hidden=true] or [inert], or under a hidden ancestor are skipped and counted in evidence.skipped, never reported. Live pages (stylegallery audit check) sample at most 24 elements evenly across the document.",
"measure": "Snapshot computed color, background-color, background-image, border-*-color, outline-style, outline-color, text-decoration-line, text-decoration-color, opacity, transform, translate, scale, box-shadow, filter, and clip-path on the element and its descendants up to depth 2. Then hover the element with the pointer and await its getAnimations({ subtree: true }) finished promises (bounded at 1000ms); snapshot again. Then move the pointer away, focus the element with the keyboard (Tab order), and snapshot a third time.",
"threshold": "0 changed properties between the rest and hover snapshots, or 0 changed properties between the rest and focus snapshots; 1 such element is enough to fail the page.",
"violation": "One error per element, reported as hover_unchanged or focus_unchanged."
"violation": "One error per element, reported as hover_unchanged, focus_unchanged, or focus_unreachable (the complete Tab walk never reached it). An element the pointer cannot reach (hover times out) or that a capped Tab walk did not reach is skipped, not a violation."
},
"rationale": "Originals put transitions on a median 76 elements per page, mostly opacity, color, transform, and background-color; studies had 8 and felt static (expression/measured-benchmarks.md, gap 4). showcase/QA.md I17 requires links, buttons, cards, tabs, and media frames to change state under the pointer, and I6 requires every focused element to be visible.",
"sources": ["expression/measured-benchmarks.md", "showcase/QA.md"]
Expand All @@ -160,7 +178,7 @@
"scope": "The whole page.",
"measure": "durations = for each visible a[href] and button with a computed transition-duration above 0, the longest duration among transitioned properties other than all-with-0s; transitioning = count of visible elements with any computed transition-duration above 0; viewports = scrollHeight / 900.",
"threshold": "median(durations) > 350ms, or median(durations) < 100ms, or transitioning < max(12, 4 * viewports). The 350ms and count floors are showcase/QA.md Q23; the 100ms lower edge is the measured band.",
"violation": "One error per page naming which bound failed."
"violation": "One error per page naming which bound failed (reasons speed and too_few). When slop.static-interactive also fails, only the speed reason is reported: controls that do not respond at all are one finding, not two."
},
"rationale": "Hover and state transitions on the originals run 100-320ms, with a median dominant duration of 200ms; studies used 400ms and felt sluggish (expression/measured-benchmarks.md). showcase/QA.md Q23 fails a page whose median link and button duration exceeds 350ms or that has fewer than 4 transitioning elements per screen of length.",
"sources": ["expression/measured-benchmarks.md", "showcase/QA.md", "motion/techniques/scroll-choreography.md"]
Expand All @@ -172,9 +190,9 @@
"detectable": "machine",
"severity": "error",
"detection": {
"scope": "Visible interactive elements outside header and footer, plus every element in header or footer whose accessible name matches /\\b(get started|sign ?up|subscribe|book|buy|order|download|try|start|join|request|contact|get (a|an|the|your)|claim|reserve)\\b/i.",
"measure": "Static: href is empty, #, #top, or javascript:, or href is #id with no element of that id. Dynamic: record location.href, scrollY, open dialogs, the text of [role=status], [role=alert], and [aria-live] regions, and outgoing requests; activate the element with a click; await one animation frame and the element's animations (bounded at 1000ms); record again.",
"threshold": "1 or more elements with a static match, or with 0 of these effects after activation: a URL change, a request, a newly open dialog or popover, an existing hash target scrolled into the top half of the viewport, or new non-empty text in a status, alert, or live region.",
"scope": "Visible interactive elements that are not skip links (/^skip (to|navigation)/i and Korean equivalents), consent or cookie controls (inside an element whose id, class, or aria-label names cookie, consent, onetrust, gdpr, or cmp, or whose name mentions cookies or consent), or legal links (privacy, terms, legal, imprint, accessibility statement), outside header and footer, plus every such element in header or footer whose accessible name matches /\\b(get started|sign ?up|subscribe|book|buy|order|download|try|start|join|request|contact|get (a|an|the|your)|claim|reserve)\\b/i.",
"measure": "Static: href is empty, #, #top, or javascript:, or href is #id with no element of that id. Dynamic: record location.href, scrollY, open dialogs, the text of [role=status], [role=alert], and [aria-live] regions, aria-expanded, aria-selected, and aria-pressed states, the rendered body text, and outgoing requests; activate the element with a click; await one animation frame and the element's animations (bounded at 1000ms); record again.",
"threshold": "1 or more elements with a static match, or with 0 of these effects after activation: a URL change, a request, a newly open dialog or popover, an existing hash target scrolled into the top half of the viewport, or new non-empty text in a status, alert, or live region, a changed aria-expanded, aria-selected, or aria-pressed state, changed rendered text, or a scroll of more than 40px. A click that times out is skipped as unreachable, not a violation. Live pages sample at most 8 calls to action.",
"violation": "One error per element. A fictional action that announces itself in a role=status message passes, as showcase/QA.md I2 and Q4 require."
},
"rationale": "showcase/QA.md I2 requires every link and button to do something plausible and fictional actions to say so in an announced status message; Q3 forbids # and #top targets and Q4 requires the status message without a scroll jump. expression/techniques/live-product-demos.md asks demos to answer honestly.",
Expand Down
10 changes: 10 additions & 0 deletions expression/anti-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ The JSON file is the source of the exact rules. It defines the shared terms once
| `slop.gradient-text-everywhere` | Gradient text as the default emphasis | Does each gradient-filled word carry a meaning the direction names? |
| `slop.uniform-section-rhythm` | Uniform section rhythm | Do sections differ in length, density, and opener so the page is paced? |

## Page Profiles

The machine thresholds were calibrated on landing and brand homepages, so they hold a reading page or an application to the wrong standard. The catalog's `profiles` object names three page profiles and, for each, the rules that become warnings instead of errors:

- `landing`: marketing, brand, and product landing pages. Every machine rule is an error; this is the calibration profile.
- `app`: dashboards, editors, search and booking forms. Richness floors, display scale, surface finishing, gradients as fill, card triplets, and motion density become warnings.
- `content`: articles, documentation, government, news, and link directories. The same rules as `app`, plus soft shadows, become warnings. A text-led screen is finished by its text, so the empty-screen floor, which counts imagery or 10% text coverage, is not a defect there.

Accessibility checks (`audit.*` other than empty screens and link transition speed), honest actions (`slop.dishonest-cta`), and emoji icons (`slop.emoji-icons`) stay errors in every profile. `stylegallery audit check` picks the profile from rendered signals unless `--profile` names one, and reports the profile, its source, and the signals. The detection rule is in `profiles.detection`. The 2026-10-19 real-site run that motivated the split is reproduced in `tests/fixtures/audit-realsites/`: GOV.UK screens with 8.5% and 9.9% text coverage, and Hacker News links without hover transitions, failed the landing floors even though they are correct for their purpose.

## Opinionated Guidance

The common thread is substitution: a gradient where a picture belongs, a shadow where a clip or hairline belongs, a bigger headline where better content belongs, and an emoji where a drawn icon belongs. The 49 measured originals keep display type at 3.5-5.5 times the body, answer every link within 100-320ms, and finish surfaces with masks and hairlines rather than soft shadows. Fixing one entry by adding the next (replacing a gradient screen with a triplet of emoji cards) still produces slop.
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,9 @@
"generate:search-embeddings": "node scripts/agent-native/v2/generate-search-embeddings.mjs",
"test:audit": "node --test scripts/test-stylegallery-audit.mjs scripts/test-audit-kit.mjs",
"test:audit:self": "node scripts/test-audit-checks.mjs",
"test:audit:parity": "node scripts/test-audit-parity.mjs"
"test:audit:parity": "node scripts/test-audit-parity.mjs",
"test:audit:realsites": "node scripts/test-audit-realsites.mjs",
"audit:realsites:live": "node scripts/audit-realsites-live.mjs"
},
"devDependencies": {
"@playwright/test": "1.61.0",
Expand Down
Loading
Loading