# Conformance rules

> Plainkit 0.28.0. Generated by scripts/build-skills.mjs from core/tools/audit/rules.mjs and core/tools/audit/examples.mjs. Do not edit: change the source and run the generator.

Every rule `npx plainkit audit` can report, one row per id: the CLI is the same rule table, so `--list-rules` lists these exact ids and `--explain <id>` shows the same detects/severity/docs. "normal" and "strict" are the finding's severity in the two modes (`off` means the rule does not run at all in that mode); run `--strict` before finishing to see errors and warnings both.

## A — accessibility attributes an element needs to have an accessible name or state

| id | normal | strict | detects |
|---|---|---|---|
| A1 | error | error | an icon-only pk-button with no accessible name |
| A2 | warn | error | a form control with neither a label attribute nor aria-label |
| A3 | error | error | a dialog/drawer without a title, a tab without text, a frame/iframe without a title, or an image without alt |
| A4 | warn | error | a positive tabindex, or a click handler on a non-interactive tag |
| A5 | warn | warn | skipped heading levels or more than one h1 in a file |
| A6 | warn | warn | a badge/alert whose variant conveys state with no text child (colour-only status) |
| A7 | warn | error | a page shell missing lang, a viewport meta, or a main landmark |
| A8 | warn | warn | a reduced-motion or touch-target override on a PlainKit element (heuristic) |

### A1

an icon-only pk-button with no accessible name

Docs: `core/elements/button/button.meta.json#a11y`

Wrong:

```text
<pk-button icon variant="ghost"></pk-button>
```

Right:

```text
<pk-button icon variant="ghost" label="Add"></pk-button>
```

### A2

a form control with neither a label attribute nor aria-label

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#25-family-a-accessibility-attributes-owner-goal-3`

Wrong:

```text
<pk-input></pk-input>
```

Right:

```text
<pk-input label="Name"></pk-input>
```

### A3

a dialog/drawer without a title, a tab without text, a frame/iframe without a title, or an image without alt

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#25-family-a-accessibility-attributes-owner-goal-3`

Wrong:

```text
<pk-dialog></pk-dialog>
```

Right:

```text
<pk-dialog heading="Discard?"></pk-dialog>
```

### A4

a positive tabindex, or a click handler on a non-interactive tag

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#25-family-a-accessibility-attributes-owner-goal-3`

Wrong:

```text
<div onclick="go()"></div>
```

Right:

```text
<pk-button>Go</pk-button>
```

### A5

skipped heading levels or more than one h1 in a file

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#25-family-a-accessibility-attributes-owner-goal-3`

Wrong:

```text
<h1>One</h1><h1>Two</h1>
```

Right:

```text
<h1>One</h1><h2>Two</h2>
```

### A6

a badge/alert whose variant conveys state with no text child (colour-only status)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#25-family-a-accessibility-attributes-owner-goal-3`

Wrong:

```text
<pk-badge variant="danger"></pk-badge>
```

Right:

```text
<pk-badge variant="danger">Failed</pk-badge>
```

### A7

a page shell missing lang, a viewport meta, or a main landmark

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#25-family-a-accessibility-attributes-owner-goal-3`

Wrong:

```text
<html><body></body></html>
```

Right:

```text
<html lang="en"><head><meta name="viewport" content="width=device-width"></head><body><main></main></body></html>
```

### A8

a reduced-motion or touch-target override on a PlainKit element (heuristic)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#25-family-a-accessibility-attributes-owner-goal-3`

Wrong:

```text
pk-button { min-height: 0; }
```

Right:

```text
pk-button { min-height: 44px; }
```

## B — Blazor and Razor only: a raw tag or a hand-styled/unknown Pk* component, a Razor-only syntax shape

| id | normal | strict | detects |
|---|---|---|---|
| B2 | warn | error | class=/style= on markup, or Class/Style on a Pk* component (S1-S3 counterpart) |
| B3 | error | error | an unknown Pk* component, or an unknown parameter on a known one (typo or removed parameter) |
| B4 | warn | warn | a @page component that does not derive from the SDK's page base |
| B5 | warn | warn | JS interop for a behaviour a component already owns (a hint, never an error: D7's Blazor counterpart) |
| B6 | warn | error | a <script> tag, an inline onclick on a non-component tag, or a MarkupString built from a non-literal value |
| B7 | warn | warn | hand-rolled lifecycle plumbing in a component: IDisposable/IAsyncDisposable + CancellationTokenSource, or busy/error fields beside OnInitializedAsync, without PageBase/PkComponentBase |
| B8 | warn | warn | a hand-written component whose [Parameter] names overlap an existing Pk* component by 80% or more (a probable duplicate, #653) |

### B2

class=/style= on markup, or Class/Style on a Pk* component (S1-S3 counterpart)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#26-family-b-blazor-and-razor-only`

Wrong:

```text
<PkButton Class="primary"></PkButton>
```

Right:

```text
<PkButton Variant="ButtonVariant.Primary"></PkButton>
```

### B3

an unknown Pk* component, or an unknown parameter on a known one (typo or removed parameter)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#26-family-b-blazor-and-razor-only`

Wrong:

```text
<PkButton Sizee="ButtonSize.Small"></PkButton>
```

Right:

```text
<PkButton Size="ButtonSize.Small"></PkButton>
```

### B4

a @page component that does not derive from the SDK's page base

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#26-family-b-blazor-and-razor-only`

### B5

JS interop for a behaviour a component already owns (a hint, never an error: D7's Blazor counterpart)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#26-family-b-blazor-and-razor-only`

Wrong:

```text
await JS.InvokeVoidAsync("dialog.showModal", elementRef);
```

Right:

```text
await dialogRef.ShowAsync();
```

### B6

a <script> tag, an inline onclick on a non-component tag, or a MarkupString built from a non-literal value

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#26-family-b-blazor-and-razor-only`

Wrong:

```text
<div onclick="go()"></div>
```

Right:

```text
<PkButton OnClick="Go">Go</PkButton>
```

### B7

hand-rolled lifecycle plumbing in a component: IDisposable/IAsyncDisposable + CancellationTokenSource, or busy/error fields beside OnInitializedAsync, without PageBase/PkComponentBase

Docs: `docs/superpowers/specs/2026-10-11-page-base-design.md#34-audit`

Wrong:

```text
@implements IDisposable @code { CancellationTokenSource? _cts; }
```

Right:

```text
@inherits PageBase @code { protected override Task OnLoadAsync(CancellationToken ct) => Task.CompletedTask; }
```

### B8

a hand-written component whose [Parameter] names overlap an existing Pk* component by 80% or more (a probable duplicate, #653)

Docs: `docs/superpowers/specs/2026-09-29-duplicate-component-detection-design.md`

Wrong:

```text
@code { [Parameter] public string? Title { get; set; } [Parameter] public string? Description { get; set; } [Parameter] public RenderFragment? ChildContent { get; set; } [Parameter] public RenderFragment? HeadingContent { get; set; } [Parameter] public RenderFragment? DescriptionContent { get; set; } }
```

Right:

```text
<PkFormSection Title="Profile"></PkFormSection>
```

## D — duplicating an element or its interaction logic instead of using the one that exists

| id | normal | strict | detects |
|---|---|---|---|
| D1 | warn | error | a raw tag that has a pk-* equivalent |
| D2 | warn | error | a hand-rolled component by class name or manual role |
| D3 | warn | error | hand-rolled pointer drag (setPointerCapture, or pointerdown + pointermove) |
| D4 | warn | error | hand-rolled arrow/Home/End keyboard navigation |
| D5 | warn | error | hand-built focus trap |
| D6 | warn | error | a manual interactive ARIA role set from script |
| D7 | warn | warn | reimplemented behaviour by platform API (a hint, never an error: the API has other legitimate uses) |
| D8 | warn | error | custom CSS styling a PlainKit element from outside (excluding documented ::part() styling) |
| D9 | warn | error | utility-class layout built by hand (flex/grid utility classes or ad hoc flex/grid CSS) |

### D1

a raw tag that has a pk-* equivalent

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#31-elements-tag-alias-and-api-hints`

Wrong:

```text
<table></table>
```

Right:

```text
<pk-table></pk-table>
```

### D2

a hand-rolled component by class name or manual role

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#31-elements-tag-alias-and-api-hints`

Wrong:

```text
<div class="modal">x</div>
```

Right:

```text
<pk-dialog>x</pk-dialog>
```

### D3

hand-rolled pointer drag (setPointerCapture, or pointerdown + pointermove)

Docs: `core/tests/composition-audit.test.mjs`

Wrong:

```text
el.addEventListener('pointerdown', start); el.addEventListener('pointermove', move);
```

Right:

```text
el.addEventListener('click', start);
```

### D4

hand-rolled arrow/Home/End keyboard navigation

Docs: `core/tests/composition-audit.test.mjs`

Wrong:

```text
el.addEventListener('keydown', e => { if (e.key === 'ArrowLeft') go(); if (e.key === 'ArrowRight') go(); });
```

Right:

```text
el.addEventListener('keydown', e => { if (e.key === 'Enter') go(); });
```

### D5

hand-built focus trap

Docs: `core/tests/composition-audit.test.mjs`

Wrong:

```text
root.querySelectorAll('[tabindex]');
```

Right:

```text
root.querySelectorAll('.item');
```

### D6

a manual interactive ARIA role set from script

Docs: `core/tests/composition-audit.test.mjs`

Wrong:

```text
node.role = 'dialog';
```

Right:

```text
node.type = 'dialog';
```

### D7

reimplemented behaviour by platform API (a hint, never an error: the API has other legitimate uses)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#31-elements-tag-alias-and-api-hints`

Wrong:

```text
dialogEl.showModal();
```

Right:

```text
dialogEl.show();
```

### D8

custom CSS styling a PlainKit element from outside (excluding documented ::part() styling)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#21-family-d-duplicating-an-element-or-its-interaction-logic-owner-goal-1`

Wrong:

```text
pk-dialog { color: red; }
```

Right:

```text
pk-dialog::part(header) { color: red; }
```

### D9

utility-class layout built by hand (flex/grid utility classes or ad hoc flex/grid CSS)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#21-family-d-duplicating-an-element-or-its-interaction-logic-owner-goal-1`

Wrong:

```text
.row { display: flex; gap: 8px; }
```

Right:

```text
.row { display: block; }
```

## P — pages and app structure: mountApp/defineModule usage versus a hand-built page

| id | normal | strict | detects |
|---|---|---|---|
| P1 | warn | error | an HTML entry page that builds its own header/nav in a <script type=module> instead of mountApp |
| P2 | warn | error | mountApp with no defineModule modules, or a defineModule missing id/title/routes |
| P3 | warn | warn | a custom mount (or moduleFromMount) whose element mix matches a built-in page type (inference; owner: stays a warning even in strict, like D2/D9) |
| P4 | warn | error | hand-built header/side nav/breadcrumbs/footer next to pk-app-shell (or its parts) |
| P5 | warn | warn | a list/record route rendering its own filter bar, pager, empty state or loading skeleton by hand |
| P6 | warn | error | localStorage/sessionStorage/history.pushState/location.hash used in app-framework code instead of defineModule({ state }) and the router |
| P7 | warn | warn | a list/record route config with no empty, error or loading handling |
| P8 | error | error | an unknown page-type id in a route (typo, or a removed built-in id) |
| P9 | warn | warn | a hand-written hash-route href/location instead of module nav/routes and ctx.navigate |
| P10 | warn | warn | a page factory that hand-rolls an AbortController, a busy/loading flag and its own dispose instead of createLifecycle |

### P1

an HTML entry page that builds its own header/nav in a <script type=module> instead of mountApp

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#23-family-p-pages-and-app-structure-owner-goal-2`

Wrong:

```text
<html><body><header>Site</header><nav>Home</nav><script type="module" src="app.js"></script></body></html>
```

Right:

```text
<html><body><script type="module">mountApp(document.body, { modules: [] });</script></body></html>
```

### P2

mountApp with no defineModule modules, or a defineModule missing id/title/routes

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#23-family-p-pages-and-app-structure-owner-goal-2`

Wrong:

```text
mountApp(document.body, { pages: { home: {} } });
```

Right:

```text
mountApp(document.body, { modules: [orders] }); defineModule({ id: 'orders', title: 'Orders', routes: [] });
```

### P3

a custom mount (or moduleFromMount) whose element mix matches a built-in page type (inference; owner: stays a warning even in strict, like D2/D9)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#23-family-p-pages-and-app-structure-owner-goal-2`

Wrong:

```text
defineModule({ id: 'orders', routes: [{ path: '/', page: 'custom', config: { mount(host) { host.innerHTML = '<pk-table></pk-table><pk-toolbar></pk-toolbar>'; } } }] });
```

Right:

```text
defineModule({ id: 'orders', routes: [{ path: '/', page: 'list', config: { columns: [] } }] });
```

### P4

hand-built header/side nav/breadcrumbs/footer next to pk-app-shell (or its parts)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#23-family-p-pages-and-app-structure-owner-goal-2`

Wrong:

```text
<pk-app-shell></pk-app-shell><header>Custom header</header><nav>Custom nav</nav>
```

Right:

```text
<pk-app-shell><pk-navbar></pk-navbar></pk-app-shell>
```

### P5

a list/record route rendering its own filter bar, pager, empty state or loading skeleton by hand

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#23-family-p-pages-and-app-structure-owner-goal-2`

Wrong:

```text
defineModule({ routes: [{ path: '/', page: 'list', config: { mount(host) { host.innerHTML = '<div class="pager">Next</div>'; } } }] });
```

Right:

```text
defineModule({ routes: [{ path: '/', page: 'list', config: { columns: [], empty: 'None yet' } }] });
```

### P6

localStorage/sessionStorage/history.pushState/location.hash used in app-framework code instead of defineModule({ state }) and the router

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#23-family-p-pages-and-app-structure-owner-goal-2`

Wrong:

```text
mountApp(document.body, {}); const q = localStorage.getItem('q');
```

Right:

```text
defineModule({ id: 'orders', state: { defaults: { q: '' } } });
```

### P7

a list/record route config with no empty, error or loading handling

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#23-family-p-pages-and-app-structure-owner-goal-2`

Wrong:

```text
defineModule({ routes: [{ path: '/', page: 'list', config: { columns: [] } }] });
```

Right:

```text
defineModule({ routes: [{ path: '/', page: 'list', config: { columns: [], empty: 'No rows', error: 'Failed', loading: true } }] });
```

### P8

an unknown page-type id in a route (typo, or a removed built-in id)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#23-family-p-pages-and-app-structure-owner-goal-2`

Wrong:

```text
defineModule({ routes: [{ path: '/', page: 'lsit', config: {} }] });
```

Right:

```text
defineModule({ routes: [{ path: '/', page: 'list', config: {} }] });
```

### P9

a hand-written hash-route href/location instead of module nav/routes and ctx.navigate

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#23-family-p-pages-and-app-structure-owner-goal-2`

Wrong:

```text
<a href="#/orders/42">Order</a>
```

Right:

```text
<pk-nav-link onclick="ctx.navigate('/orders/42')">Order</pk-nav-link>
```

### P10

a page factory that hand-rolls an AbortController, a busy/loading flag and its own dispose instead of createLifecycle

Docs: `docs/superpowers/specs/2026-10-11-page-base-design.md#34-audit`

Wrong:

```text
let busy = false; const ac = new AbortController(); function destroy() { ac.abort(); }
```

Right:

```text
import { createLifecycle } from 'plainkit/page-lifecycle.js'; const life = createLifecycle({ page, signal });
```

## S — strict-module standards (security, styling, structure) carried over for a consumer app

| id | normal | strict | detects |
|---|---|---|---|
| S1 | warn | error | own CSS files, @import, <link rel=stylesheet>, adoptedStyleSheets, <style> |
| S2 | warn | error | style attribute or property (style=, .style, cssText, setProperty) |
| S3 | warn | error | class, className, classList |
| S4 | warn | error | raw standard tags with a pk-* equivalent (superseded in a consumer run by D1's hint; S4 is the strict id) |
| S5 | warn | error | HTML sinks (innerHTML, outerHTML, insertAdjacentHTML, document.write, DOMParser, createContextualFragment, srcdoc) |
| S6 | off | error | imports outside the allowed set (a consumer names its own libraries; approximate without config) |
| S7 | off | warn | direct platform access (document., window., location, localStorage, addEventListener, timers, fetch) |
| S8 | warn | error | custom page type, moduleFromMount, module-defined pageTypes/layouts |
| S9 | warn | error | literal design values (colours, lengths with units, z-index, font names) |

### S1

own CSS files, @import, <link rel=stylesheet>, adoptedStyleSheets, <style>

Docs: `core/STANDARDS.md#ownership-and-reactivity`

Wrong:

```text
.a { color: red; }
```

Right:

```text
const x = 1;
```

### S2

style attribute or property (style=, .style, cssText, setProperty)

Docs: `core/STANDARDS.md#ownership-and-reactivity`

Wrong:

```text
el.style.color = 'red';
```

Right:

```text
const width = compute();
```

### S3

class, className, classList

Docs: `core/STANDARDS.md#ownership-and-reactivity`

Wrong:

```text
<div class="foo"></div>
```

Right:

```text
<div></div>
```

### S4

raw standard tags with a pk-* equivalent (superseded in a consumer run by D1's hint; S4 is the strict id)

Docs: `core/STANDARDS.md#composition`

Wrong:

```text
<button>Click</button>
```

Right:

```text
<pk-button>Click</pk-button>
```

### S5

HTML sinks (innerHTML, outerHTML, insertAdjacentHTML, document.write, DOMParser, createContextualFragment, srcdoc)

Docs: `AGENTS.md#rules-that-are-easy-to-break`

Wrong:

```text
el.innerHTML = '<b>hi</b>';
```

Right:

```text
el.textContent = 'hi';
```

### S6

imports outside the allowed set (a consumer names its own libraries; approximate without config)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#22-the-strict-module-rules-for-consumers-s-family-from-515`

Wrong:

```text
import foo from 'some-lib';
```

Right:

```text
import { mountApp } from 'plainkit';
```

### S7

direct platform access (document., window., location, localStorage, addEventListener, timers, fetch)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#22-the-strict-module-rules-for-consumers-s-family-from-515`

Wrong:

```text
document.querySelector('a');
```

Right:

```text
state.value = 1;
```

### S8

custom page type, moduleFromMount, module-defined pageTypes/layouts

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#23-family-p-pages-and-app-structure-owner-goal-2`

Wrong:

```text
mountApp({ page: 'custom' });
```

Right:

```text
mountApp({ page: 'list' });
```

### S9

literal design values (colours, lengths with units, z-index, font names)

Docs: `AGENTS.md#rules-that-are-easy-to-break`

Wrong:

```text
.a { color: #ff0000; }
```

Right:

```text
.a { color: var(--color-danger); }
```

## T — tokens and standards: literal colours/sizes/fonts, deprecated names, the wrong import path

| id | normal | strict | detects |
|---|---|---|---|
| T1 | warn | error | a literal colour in app CSS (hex, rgb()/rgba(), hsl()/hsla()) |
| T2 | warn | error | a literal size (px/rem/em length for spacing, radius or font-size) |
| T3 | warn | error | a font-family or font-size literal outside the token set |
| T4 | error | error | a removed focus ring (outline: none/0) with no replacement in the same rule |
| T5 | warn | error | an unknown CSS custom property (var(--x) where --x is neither a PlainKit token nor declared in the file) |
| T6 | error | error | an unknown pk-* element (typo) or an enum attribute value outside its declared values |
| T7 | warn | error | a deprecated or removed pk-* element or attribute |
| T8 | warn | warn | importing the whole element bundle instead of the per-element modules (info-grade) |

### T1

a literal colour in app CSS (hex, rgb()/rgba(), hsl()/hsla())

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#24-family-t-tokens-and-standards-owner-goal-3`

Wrong:

```text
.a { color: #ff0000; }
```

Right:

```text
.a { color: var(--color-danger); }
```

### T2

a literal size (px/rem/em length for spacing, radius or font-size)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#24-family-t-tokens-and-standards-owner-goal-3`

Wrong:

```text
.a { padding: 12px; }
```

Right:

```text
.a { padding: var(--space-3); }
```

### T3

a font-family or font-size literal outside the token set

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#24-family-t-tokens-and-standards-owner-goal-3`

Wrong:

```text
.a { font-family: 'Arial', sans-serif; }
```

Right:

```text
.a { font-family: var(--font-sans); }
```

### T4

a removed focus ring (outline: none/0) with no replacement in the same rule

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#24-family-t-tokens-and-standards-owner-goal-3`

Wrong:

```text
.a:focus { outline: none; }
```

Right:

```text
.a:focus { outline: none; box-shadow: 0 0 0 2px blue; }
```

### T5

an unknown CSS custom property (var(--x) where --x is neither a PlainKit token nor declared in the file)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#33-tokens`

Wrong:

```text
.a { color: var(--colr-typo); }
```

Right:

```text
.a { color: var(--color-critical); }
```

### T6

an unknown pk-* element (typo) or an enum attribute value outside its declared values

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#31-elements-tag-alias-and-api-hints`

Wrong:

```text
<pk-buttonn>Save</pk-buttonn>
```

Right:

```text
<pk-button>Save</pk-button>
```

### T7

a deprecated or removed pk-* element or attribute

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#31-elements-tag-alias-and-api-hints`

### T8

importing the whole element bundle instead of the per-element modules (info-grade)

Docs: `docs/superpowers/specs/2026-09-28-conformance-audit-cli-design.md#24-family-t-tokens-and-standards-owner-goal-3`

Wrong:

```text
import { PkButton } from 'plainkit/elements/elements.js';
```

Right:

```text
import { PkButton } from 'plainkit/elements/button.js';
```
