affora 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-09-24
4
+
5
+ Initial public release.
6
+
7
+ - Copy-in CLI with listing, dependency resolution, dry runs, collision safety,
8
+ custom destinations, and complete-registry installation.
9
+ - Two native primitives: Action button and Text field.
10
+ - Five compositions: Combobox, Dialog, Product card, Data table, and Settings
11
+ form.
12
+ - Five flow patterns: Gated action, Error remedy, Confirm and undo, Flow form,
13
+ and Persistent feedback.
14
+ - Sixteen token themes, executable substrate checks, and rewrite rules for
15
+ common component libraries.
16
+ - Human-facing `design.md` and coding-agent-facing `agent.md` contracts.
package/CHECKS.md ADDED
@@ -0,0 +1,40 @@
1
+ # The eight component checks
2
+
3
+ Each check is a predicate over a rendered component. Each returns a verdict and
4
+ the evidence it read, and each has an input that must fail it.
5
+
6
+ | # | check | how it is decided |
7
+ |---|---|---|
8
+ | S1 | every interactive element is a real control or carries a role | every element that behaves as a control is an element with a role |
9
+ | S2 | every control has a non-empty accessible name | name computation over each control |
10
+ | S3 | every task-relevant fact is present as text | the task's facts occur in the rendered text |
11
+ | S4 | selection or expansion state is exposed semantically | state is on the element, not in a class |
12
+ | S5 | options are enumerable without interacting | the action space at rest holds at least the task's options, counting a native select, an ARIA listbox, and a radio or checkbox group |
13
+ | S6 | the operable target coincides with the visible one | no control is sized to zero or transparent behind a proxy, and a hit test at the visible centre lands inside |
14
+ | S7 | the committed value is readable back as text | after a commit, the value occurs in the rendered text |
15
+ | S8 | meaning never rests on colour or position alone | no element is distinguished by appearance alone |
16
+
17
+ Three verdicts, not two: a check that cannot be decided from what it was given
18
+ returns **undecided**. S3, S5 and S7 need the task's facts, and given none they
19
+ say so rather than passing.
20
+
21
+ ## Running them
22
+
23
+ ```bash
24
+ node checks/cli.mjs <url> [selector] [fact,fact,...]
25
+ ```
26
+
27
+ For example, against the demo:
28
+
29
+ ```bash
30
+ npm run demo &
31
+ node checks/cli.mjs http://localhost:5280/ "section:nth-of-type(1)" "Small,Medium,Large"
32
+ ```
33
+
34
+ ## The three page checks
35
+
36
+ A rewrite of a page you did not author has to be checked in the other direction
37
+ as well: it must not destroy what was there. C1 asks whether the page still says
38
+ everything it said; C2 whether it still has every affordance; C3 whether using a
39
+ control still causes the same effect outside the page. Two defects in the rules
40
+ under `rules/` were found by C2 and C3 and one from an anomalous success rate.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Affora authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # Affora
2
+
3
+ A component library for interfaces with two readers: a person, and an agent that
4
+ operates the page by reading it.
5
+
6
+ Affora is not a runtime. It is source you copy into your own repository, the way
7
+ `shadcn/ui` is, plus a token vocabulary you own and a compliance test you can run
8
+ in CI. One rule holds it together, at three scales:
9
+
10
+ > **What an interface declares is fixed and checked. What it paints is free.**
11
+
12
+ At component scale that reads *substrate invariant, skin variable*: a component
13
+ is authored once, and every visual decision is a token.
14
+
15
+ ## What is here
16
+
17
+ ```
18
+ src/primitives/ native actions and fields with names, constraints, and state
19
+ src/components/ five compositions, authored once, every visual value a token
20
+ src/patterns/ flow-pattern sources; publishable patterns are listed by the CLI
21
+ src/tokens/ 45 style tokens and 7 layout tokens, and 16 themes that set them
22
+ checks/ the eight component checks, as a command
23
+ rules/ rewrite rules that repair a page you did not author
24
+ demo/ every component under every theme, in a browser
25
+ design.md the human-facing substrate and skin contract
26
+ agent.md the operating contract a coding agent reads before editing
27
+ ```
28
+
29
+ ## Using a component
30
+
31
+ Use the copy-in CLI. A component has no Affora import and no runtime dependency
32
+ beyond React; its token dependency is copied with it:
33
+
34
+ ```bash
35
+ npx affora add product-card
36
+ ```
37
+
38
+ This writes `src/components/affora/product-card.tsx` and
39
+ `src/styles/affora/themes.css`. Run `npx affora list` to see the registry, use
40
+ `--path <directory>` to choose another root, and use `--dry-run` to inspect a
41
+ copy plan. Run `npx affora add --all` to install the complete registry. The CLI
42
+ refuses to overwrite files unless you pass `--force`.
43
+
44
+ Requires Node.js 18 or later. Copied React components support React 18 and later.
45
+
46
+ Then set a theme by attribute, and the component follows:
47
+
48
+ ```html
49
+ <html data-flagship="swiss" data-layout="saas">
50
+ ```
51
+
52
+ A theme may redefine all 45 style tokens and all 7 layout tokens. It may not
53
+ reach the component's markup, which is the point: the reader that enumerates
54
+ sees the same document under every theme.
55
+
56
+ ## Running the checks
57
+
58
+ The eight component checks (S1–S8) decide, from the rendered document, whether a
59
+ component is legible to a reader that enumerates an action space:
60
+
61
+ ```bash
62
+ npm run check -- https://example.test '#checkout' 'Cart total,Place order'
63
+ ```
64
+
65
+ Arguments are the page URL, an optional component selector, and an optional
66
+ comma-separated list of task-relevant facts. The runner launches Chromium and
67
+ prints every verdict with the evidence it read.
68
+
69
+ Each check reports a verdict and the evidence it read. A check that cannot be
70
+ decided from what it was given returns *undecided* rather than *pass*. The CLI
71
+ exits non-zero when a check fails; callers that require full conformance should
72
+ also treat an *undecided* result as missing evidence.
73
+
74
+ **What passing means.** The document contains, for every component on the page, a
75
+ real control, an accessible name, state exposed as state, options enumerable
76
+ without interacting, and an operable target where it visibly sits. That is a
77
+ property of the artefact, decided without running a model. It is not a
78
+ prediction that an agent will succeed: failures also come from transient
79
+ feedback, portalled menus and flow state, which a component-scoped check does not
80
+ see. Treat it the way you treat a type checker.
81
+
82
+ ## Repairing a page you did not author
83
+
84
+ `rules/` holds the rewrite rules used in the paper's evaluation. They detect
85
+ common library idioms — a combobox whose options mount only when open, an
86
+ icon-only control, a radio group hidden by CSS behind styled labels — and
87
+ re-express them so the options and names are in the document.
88
+
89
+ The rule files are browser-evaluated rule objects consumed by Affora's rewrite
90
+ harness; they are shipped as auditable source, not presented as a standalone
91
+ command. A consumer must run the generic rules first, then the matching library
92
+ adapter, and apply the page gates below around every rewrite.
93
+
94
+ A rewrite can damage a page it did not author. Before measuring anything, run the
95
+ three page gates: the page must still **say** everything (C1), still **have**
96
+ every affordance (C2), and using it must still **cause** the same effect (C3).
97
+ Two defects in these rules were found that way and one from an anomalous success
98
+ rate; all three are described in the paper.
99
+
100
+ ## Licence
101
+
102
+ MIT.
103
+
104
+ ## Reproducing the paper's figures from this library
105
+
106
+ The five components here are the ones the paper measures, byte for byte, and the
107
+ sixteen themes are the ones its theme axis varies. `npm run demo` renders every
108
+ component under every theme and layout, with the committed value shown beside the
109
+ controls, which is the probe every episode in the paper reads.
110
+
111
+ ## What is not here
112
+
113
+ The measurement harness, the task sets and the episode traces are not part of the
114
+ library; they are research code and they are described in the paper. What ships
115
+ here is what a team would adopt: the components, the tokens, the patterns, the
116
+ checks and the rewrite rules.
package/agent.md ADDED
@@ -0,0 +1,70 @@
1
+ # Affora operating contract for coding agents
2
+
3
+ Read this file before changing an interface that uses Affora.
4
+
5
+ ## Goal
6
+
7
+ Preserve one semantic document for two readers: a person who sees the rendered
8
+ page and an agent that enumerates its controls, names, relationships, and state.
9
+ Visual design is free to change; the task-relevant substrate is not.
10
+
11
+ ## Required behaviour
12
+
13
+ - Use native HTML controls before simulated controls.
14
+ - Give every interactive element a stable accessible name. Prefer visible text.
15
+ - Keep task-relevant options and facts in the DOM without requiring interaction.
16
+ - Express selected, current, expanded, invalid, busy, and completed state with
17
+ native state, ARIA where appropriate, and persistent text when the fact matters
18
+ to the task.
19
+ - State constraints before submission. A disabled action must name its blocker
20
+ and the action or control that removes it.
21
+ - Keep action results and errors readable. An error states what failed, why, and
22
+ the named corrective action. Do not make a transient toast the only record.
23
+ - Make every step self-describing. Restate facts and named progress needed after
24
+ navigation or context loss.
25
+ - Keep visible and operable targets coincident. Do not place a tiny hidden input
26
+ behind a larger visual control unless the actual hit target covers that control.
27
+ - Mark destructive consequences in text. Prefer undo for reversible operations.
28
+ - Preserve semantic element order and relationships across themes and layouts.
29
+
30
+ ## Vocabulary
31
+
32
+ - **primitive**: one native interaction
33
+ - **composition**: primitives expressing one object
34
+ - **flow pattern**: state and actions across time
35
+ - **substrate**: semantic document and task-relevant state
36
+ - **skin**: tokens, layout, typography, colour, motion, and effects
37
+
38
+ Do not introduce alternate layer names or unexplained check identifiers into
39
+ user-facing documentation.
40
+
41
+ ## Adding a registry component
42
+
43
+ 1. Add one self-contained React source file under `src/primitives/`,
44
+ `src/components/`, or `src/patterns/`.
45
+ 2. Depend only on React and Affora tokens. Do not import experiment shims or use
46
+ the `@/` alias.
47
+ 3. Export the component and its token-only CSS. Do not bake a theme into it.
48
+ 4. Add the item to `registry.json`; declare `themes` in `requires`.
49
+ 5. Add or update CLI tests when dependency or destination behaviour changes.
50
+ 6. Run `npm test`, then install the output of `npm pack` into a temporary project
51
+ and execute the installed `affora` binary.
52
+
53
+ Never add a file to the registry while it imports control-condition shims,
54
+ experiment types, or a transient-feedback dependency. Registry items are
55
+ standalone reference implementations.
56
+
57
+ ## Safe changes
58
+
59
+ Safe changes alter tokens, styling, or composition without changing controls,
60
+ names, option presence, state exposure, or effects. A refactor is not safe merely
61
+ because the pixels are unchanged.
62
+
63
+ For any substrate change, compare before and after and answer:
64
+
65
+ 1. Does the page still say every relevant fact and state?
66
+ 2. Does it still have every named affordance?
67
+ 3. Does each affordance still cause the same effect?
68
+
69
+ Run the executable checks after answering those questions. Treat `undecided` as
70
+ missing evidence, not a pass.
package/checks/cli.mjs ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // Run the eight component checks against a page.
3
+ //
4
+ // node checks/cli.mjs <url> [selector] [fact,fact,...]
5
+ //
6
+ // The selector picks the component to check; it defaults to the whole document
7
+ // body. Every verdict carries the evidence the check read, and a check that
8
+ // cannot be decided from what it was given says so rather than passing.
9
+ import { RUN } from "./substrate.mjs"
10
+
11
+ // Two checks — every task-relevant fact present as text, and options
12
+ // enumerable — need to know which facts the task depends on. Given none they
13
+ // return undecided rather than pass, which is the point of the third verdict.
14
+ const [url, selector = "body", factsArg = ""] = process.argv.slice(2)
15
+ const facts = factsArg ? factsArg.split(",").map((f) => f.trim()).filter(Boolean) : []
16
+ if (!url) {
17
+ console.error("usage: node checks/cli.mjs <url> [selector] [fact,fact,...]")
18
+ process.exit(2)
19
+ }
20
+ const { chromium } = await import("playwright")
21
+ const browser = await chromium.launch()
22
+ const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
23
+ await page.goto(url, { waitUntil: "networkidle" })
24
+ await page.waitForTimeout(1200)
25
+
26
+ // RUN addresses its subject through a data attribute, so the page under test is
27
+ // marked once rather than the predicate being rewritten per host.
28
+ await page.evaluate((sel) => {
29
+ const el = document.querySelector(sel) || document.body
30
+ el.setAttribute("data-component", "subject")
31
+ }, selector)
32
+
33
+ const results = await page.evaluate(RUN, { comp: "subject", facts, expected: null, arm: "subject" })
34
+ await browser.close()
35
+
36
+ let failed = 0
37
+ for (const r of results) {
38
+ const verdict = r.pass === true ? "pass" : r.pass === false ? "FAIL" : "undecided"
39
+ if (r.pass === false) failed += 1
40
+ console.log(`${r.id} ${verdict.padEnd(9)} ${r.label}`)
41
+ if (r.evidence) console.log(` ${String(r.evidence).replace(/\s+/g, " ").slice(0, 96)}`)
42
+ }
43
+ console.log(`\n${results.filter((r) => r.pass === true).length}/${results.length} pass, ${failed} fail`)
44
+ process.exit(failed ? 1 : 0)
@@ -0,0 +1,269 @@
1
+ // The substrate checklist, S1-S8, as executable predicates.
2
+ //
3
+ // Extracted from the figure script so the checker is an artifact rather than
4
+ // figure-generation code. The paper claims Affora's component checks are
5
+ // executed rather than judged — that expert disagreement, which Brajnik et al.
6
+ // measure for WCAG conformance, is not a variable for this layer because no
7
+ // human adjudicates it. A claim like that has to be a module somebody can
8
+ // import and run, not a paragraph.
9
+ //
10
+ // RUN is passed to page.evaluate(), so it must be self-contained: no imports,
11
+ // no closure over module scope. It takes the component id, the task's facts and
12
+ // its expected value, and returns one record per check carrying a verdict AND
13
+ // the evidence the check read. A check that reports only a verdict cannot be
14
+ // argued with; one that reports what it saw can.
15
+ //
16
+ // Scope, stated because the paper states it: these are the COMPONENT-level
17
+ // checks. The flow checklist (L1-L8) needs a task path rather than a screen and
18
+ // the site checklist (K1-K3) needs several routes; neither is here.
19
+
20
+ // Each check is a predicate over the rendered component plus the task's own
21
+ // facts, and each returns the evidence it read.
22
+ export const RUN = ({ comp, facts, expected, arm }) => {
23
+ const cell = document.querySelector(`[data-component="${comp}"]`)
24
+ const SEL = "a,button,input,select,textarea,summary,[role]"
25
+ const vis = (e) => {
26
+ const r = e.getBoundingClientRect(), cs = getComputedStyle(e)
27
+ return r.width > 0 && r.height > 0 && cs.visibility !== "hidden" && cs.display !== "none"
28
+ }
29
+ const raw = [...cell.querySelectorAll(SEL)]
30
+ const els = raw.filter(vis)
31
+
32
+ // S6 has to look at the elements the visibility filter throws away, because
33
+ // the defect it names IS invisibility: a real control sized to zero or made
34
+ // transparent behind a styled proxy. Filtering first made the check blind to
35
+ // its only subject — the falsification case returned "no operable target"
36
+ // instead of failing.
37
+ const hidden = raw.filter((e) => {
38
+ if (vis(e)) return false
39
+ if (e.disabled || e.getAttribute("aria-hidden") === "true") return false
40
+ const r = e.getBoundingClientRect(), cs = getComputedStyle(e)
41
+ return cs.display !== "none" && (r.width < 2 || r.height < 2 || Number(cs.opacity) === 0)
42
+ })
43
+ const NATIVE = ["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA", "SUMMARY", "OPTION"]
44
+ const nameOf = (e) => (e.getAttribute("aria-label") || e.textContent || e.getAttribute("placeholder") || e.value || "").trim().replace(/\s+/g, " ")
45
+ // A component with no real control at all is the strongest S1 failure there
46
+ // is, and the checker used to throw on it: els[0] was undefined and every
47
+ // check dereferenced it. Four components in our own corpus hit this, so the
48
+ // instrument was silently unable to score its worst cases.
49
+ if (els.length === 0 && hidden.length) {
50
+ // real controls exist and none of them is visible: that is S6's defect in
51
+ // its purest form, not an absence of controls
52
+ const R1 = (id, label, pass, evidence, wcag) => ({ id, label, pass, evidence, wcag })
53
+ return [
54
+ R1("S1", "every interactive element is a real control or carries a role", true,
55
+ `${hidden.length} real control(s) present`, "4.1.2"),
56
+ R1("S2", "every control has a non-empty accessible name", null, "no visible control to name", "4.1.2"),
57
+ R1("S3", "every task-relevant fact is present as text", null, "no visible control to scope to", null),
58
+ R1("S4", "selection or expansion state is exposed semantically", null, "no visible control", "4.1.2"),
59
+ R1("S5", "options are enumerable without interacting", null, "no visible control", null),
60
+ R1("S6", "the operable target coincides with the visible one", false,
61
+ `${hidden.length} operable control(s) sized to zero or transparent`, null),
62
+ R1("S7", "the committed value is readable back as text", null, "no visible control", null),
63
+ R1("S8", "meaning never rests on colour or position alone", null, "no visible control", "1.4.1"),
64
+ ]
65
+ }
66
+ if (els.length === 0) {
67
+ const R0 = (id, label, pass, evidence, wcag) => ({ id, label, pass, evidence, wcag })
68
+ return [
69
+ R0("S1", "every interactive element is a real control or carries a role", false,
70
+ "no real control and no role in the component", "4.1.2"),
71
+ R0("S2", "every control has a non-empty accessible name", null, "no control to name", "4.1.2"),
72
+ R0("S3", "every task-relevant fact is present as text", null, "no control to scope the text to", null),
73
+ R0("S4", "selection or expansion state is exposed semantically", null, "no control to carry state", "4.1.2"),
74
+ R0("S5", "options are enumerable without interacting", null, "no control to enumerate from", null),
75
+ R0("S6", "the operable target coincides with the visible one", null, "no operable target", null),
76
+ R0("S7", "the committed value is readable back as text", null, "no control to commit", null),
77
+ R0("S8", "meaning never rests on colour or position alone", null, "no control to judge", "1.4.1"),
78
+ ]
79
+ }
80
+ const ctl = els[0]
81
+ // Everything a reader can choose from without opening anything: a native
82
+ // select's options, an ARIA listbox's options, and a native radio or checkbox
83
+ // group, which is the form this system prescribes and which this check did
84
+ // not count — it failed the library's own compliant product card, whose sizes
85
+ // are three labelled radios.
86
+ const groupLabel = (e) => {
87
+ const byFor = e.id && cell.querySelector(`label[for="${CSS.escape(e.id)}"]`)
88
+ const wrap = e.closest("label")
89
+ return ((byFor && byFor.textContent) || (wrap && wrap.textContent) ||
90
+ e.getAttribute("aria-label") || e.value || "").replace(/\s+/g, " ").trim()
91
+ }
92
+ const options = [
93
+ ...[...cell.querySelectorAll("select")].flatMap((s) => [...s.options].map((o) => (o.label || "").trim())),
94
+ ...[...cell.querySelectorAll("[role=option]")].map((o) => o.textContent.trim()),
95
+ ...[...cell.querySelectorAll('input[type="radio"], input[type="checkbox"]')].map(groupLabel),
96
+ ].filter((o) => o && o !== "Select a country")
97
+ // The component's own text, and all of it.
98
+ //
99
+ // This was scoped to the control's field wrapper, to keep the gallery's arm
100
+ // label and harness probe out of the figure's evidence strings. That is the
101
+ // wrong scope for S7, which asks whether a committed value is readable back
102
+ // as DOCUMENT text: a value echoed in a sibling paragraph is readable, and
103
+ // the field-scoped version called it missing. Falsifying S7 is what surfaced
104
+ // it — the case built to make the check PASS failed instead.
105
+ //
106
+ // So the scope is the whole component minus the harness's own instrumentation:
107
+ // the `output[data-expected]` probe the runner reads, and the arm-label header
108
+ // the gallery draws. Both are ours, neither is the component.
109
+ // Walked in place rather than cloned: innerText needs layout, and a detached
110
+ // clone silently falls back to textContent, which runs block elements together
111
+ // ("CountrySelect a country") and made the figure's evidence look mangled.
112
+ const isChrome = (el) =>
113
+ el.matches?.("output[data-expected]") ||
114
+ (arm && (el.textContent || "").trim().toUpperCase().startsWith(arm.toUpperCase()))
115
+ const text = [...cell.children]
116
+ .filter((el) => !isChrome(el))
117
+ .map((el) => el.innerText || "")
118
+ .join(" ")
119
+ .replace(/\s+/g, " ")
120
+ .trim()
121
+
122
+ const box = ctl.getBoundingClientRect()
123
+ // the element the browser would actually deliver a click on the control to
124
+ const hit = document.elementFromPoint(box.left + box.width / 2, box.top + box.height / 2)
125
+ // control kinds that HAVE a state a reader could need: everything else has
126
+ // nothing for S4 to ask about
127
+ const STATEFUL_ROLES = ["checkbox", "radio", "switch", "tab", "option", "slider",
128
+ "combobox", "menuitemcheckbox", "menuitemradio", "treeitem", "spinbutton"]
129
+ const stateful = els.filter((e) =>
130
+ e.tagName === "SELECT" || e.tagName === "SUMMARY" ||
131
+ (e.tagName === "INPUT" && ["checkbox", "radio", "range"].includes(e.type)) ||
132
+ STATEFUL_ROLES.includes(e.getAttribute("role") || "") ||
133
+ e.hasAttribute("aria-expanded") || e.hasAttribute("aria-checked") ||
134
+ e.hasAttribute("aria-selected") || e.hasAttribute("aria-pressed"))
135
+ const hits = ctl.contains(hit) || hit === ctl || ctl.contains(hit?.parentElement)
136
+ const stateAttr = ["aria-expanded", "aria-checked", "aria-selected", "aria-valuenow", "aria-pressed"]
137
+ .find((a) => els.some((e) => e.hasAttribute(a)))
138
+ const stateEl = els.find((e) => stateAttr && e.hasAttribute(stateAttr))
139
+
140
+ // pass is true, false, or null for "this check cannot be decided from what it
141
+ // was given". Null is NOT a pass. Two checks need to know which facts the task
142
+ // involves, and called with an empty fact list they were vacuously true —
143
+ // `facts.every(...)` on nothing, `options.length >= 0` — so the standalone
144
+ // checker reported a clean bill of health for a component it had not examined.
145
+ // A gate that cannot fail is not a gate; this is the shape that failure takes
146
+ // in a checklist rather than in a benchmark.
147
+ // the value a control is currently holding, if any — what S7 looks for
148
+ const heldBy = (e) => {
149
+ if (e.tagName === "SELECT") { const o = e.selectedOptions[0]; return o && o.value ? (o.label || o.textContent || "").trim() : null }
150
+ if (e.tagName === "INPUT" && ["checkbox", "radio"].includes(e.type)) return e.checked ? (e.value || "on") : null
151
+ if (e.tagName === "INPUT" || e.tagName === "TEXTAREA") return e.value ? String(e.value).trim() : null
152
+ const sel = e.getAttribute("aria-checked") === "true" || e.getAttribute("aria-selected") === "true"
153
+ if (sel) return (e.textContent || "").trim() || null
154
+ const now = e.getAttribute("aria-valuenow")
155
+ return now != null ? String(now) : null
156
+ }
157
+ const held = els.map(heldBy).find((v) => v) ?? null
158
+
159
+ // elements distinguished by appearance with nothing a reader could read
160
+ const colourOnly = [...cell.querySelectorAll("span,div,i,b")].filter((e) => {
161
+ const r = e.getBoundingClientRect()
162
+ if (r.width <= 0 || r.height <= 0 || r.width > 64 || r.height > 64) return false
163
+ if (e.children.length) return false
164
+ if ((e.textContent || "").trim()) return false
165
+ if (e.getAttribute("aria-label") || e.getAttribute("title") || e.getAttribute("role")) return false
166
+ if (e.getAttribute("aria-hidden") === "true") return false
167
+ const cs = getComputedStyle(e)
168
+ const painted = (cs.backgroundColor && cs.backgroundColor !== "rgba(0, 0, 0, 0)") ||
169
+ (cs.borderTopWidth !== "0px" && cs.borderTopStyle !== "none")
170
+ return painted
171
+ }).map((e) => `${e.tagName.toLowerCase()}.${(e.className || "").toString().split(" ")[0] || "-"}`)
172
+
173
+ const R = (id, label, pass, evidence, wcag) => ({ id, label, pass, evidence, wcag })
174
+ const needsFacts = facts.length === 0
175
+ return [
176
+ R("S1", "every interactive element is a real control or carries a role",
177
+ els.every((e) => NATIVE.includes(e.tagName) || e.getAttribute("role")),
178
+ `${ctl.tagName.toLowerCase()}${ctl.getAttribute("role") ? `[role=${ctl.getAttribute("role")}]` : ""}`,
179
+ "4.1.2"),
180
+ R("S2", "every control has a non-empty accessible name",
181
+ els.every((e) => nameOf(e)), `"${nameOf(ctl)}"`, "4.1.2"),
182
+ R("S3", "every task-relevant fact is present as text",
183
+ needsFacts ? null : facts.every((f) => text.includes(f)),
184
+ `rendered text: "${text.length > 46 ? text.slice(0, 46) + "\u2026" : text}"`,
185
+ // Not 1.3.1. That criterion governs how PRESENTED content is marked up so
186
+ // its structure is programmatically determinable; a collapsed control
187
+ // presents no options, so it does not engage the criterion at all. Mapping
188
+ // this row to 1.3.1 would have overstated what conformance asks for.
189
+ null),
190
+ // S4 asks whether state is exposed, and asked it of components that have no
191
+ // state to expose: run over 120 corpus cells it failed 55 of 60 native ones,
192
+ // because a plain button carries no aria-expanded and is not a select. A
193
+ // check that fires on components it does not apply to is a false-positive
194
+ // machine, and its correlation with success was partly noise. It is scoped
195
+ // to components that contain a stateful control kind; where none does, the
196
+ // check is undecidable rather than failing.
197
+ R("S4", "selection or expansion state is exposed semantically",
198
+ stateful.length === 0 ? null
199
+ : (Boolean(stateAttr) || stateful.some((e) =>
200
+ e.tagName === "SELECT" || e.tagName === "SUMMARY" ||
201
+ (e.tagName === "INPUT" && ["checkbox", "radio", "range"].includes(e.type)))),
202
+ stateful.length === 0 ? "no stateful control in this component"
203
+ : (!stateAttr && stateful.some((e) => ["SELECT", "SUMMARY", "INPUT"].includes(e.tagName)))
204
+ ? `${stateful[0].tagName.toLowerCase()} carries its state natively`
205
+ : stateAttr ? `${stateEl.tagName.toLowerCase()} ${stateAttr}="${stateEl.getAttribute(stateAttr)}"`
206
+ : `${ctl.tagName.toLowerCase()} carries its own value`,
207
+ "4.1.2"),
208
+ R("S5", "options are enumerable without interacting",
209
+ needsFacts ? null : options.length >= facts.length,
210
+ needsFacts ? "needs the task's facts to decide"
211
+ : options.length ? `action space: ${options.slice(0, 3).join(", ")}${options.length > 3 ? `, +${options.length - 3}` : ""}`
212
+ : "action space: the trigger only",
213
+ null),
214
+ R("S6", "the operable target coincides with the visible one",
215
+ hidden.length === 0 && hits,
216
+ // The evidence has to say WHICH condition decided it. This printed
217
+ // "hit test lands inside" on cells it had just marked failing, because the
218
+ // string was chosen by hidden.length alone — so a hit-test miss was
219
+ // reported with the words for a pass, and reading the corpus output nearly
220
+ // turned two of those into a claim about zero-sized controls.
221
+ hidden.length
222
+ ? `${hidden.length} operable control(s) sized to zero or transparent`
223
+ : hits
224
+ ? `${Math.round(box.width)}×${Math.round(box.height)} px, hit test lands inside`
225
+ : `${Math.round(box.width)}×${Math.round(box.height)} px, but the point at its centre belongs to <${(hit?.tagName || "nothing").toLowerCase()}>`,
226
+ null),
227
+ // S7 and S8 were both hardcoded `true` — literally `R("S7", ..., true, ...)`.
228
+ // They were written while building the figure, where both arms pass, so two
229
+ // constants sat in the middle of a checklist looking like checks. Run over
230
+ // 120 corpus cells neither ever fired, which is how they were caught. A gate
231
+ // that cannot fail is not a gate, and this project keeps relearning it.
232
+ //
233
+ // S7 now asks the question it names: does a value a control is holding
234
+ // appear in the component's rendered text? Undecidable when no control holds
235
+ // one yet, because a static snapshot of an untouched component has no
236
+ // committed value to look for.
237
+ // The failure this names is a value that lives in a control's `value`
238
+ // property and nowhere in the document — the case that moved a memoryless
239
+ // arm from 0% to 33% once the extractor was taught to read it. So the
240
+ // question is whether the committed value is in the rendered TEXT. It needs
241
+ // a value to have been committed: given one (the caller drives the
242
+ // component first) it is decidable, and a static snapshot of an untouched
243
+ // component honestly cannot decide it.
244
+ R("S7", "the committed value is readable back as text",
245
+ expected ? text.includes(expected) : (held === null ? null : text.includes(held)),
246
+ expected
247
+ ? `committed "${expected}"; rendered text ${text.includes(expected) ? "contains" : "does NOT contain"} it`
248
+ : held === null
249
+ ? "no value has been committed in this state"
250
+ : `control holds "${held}"; rendered text ${text.includes(held) ? "contains" : "does NOT contain"} it`,
251
+ null),
252
+ // S8's mechanical proxy: an element that is visually distinguished and
253
+ // carries no text, no accessible name and no ARIA state is conveying
254
+ // whatever it conveys by appearance alone. This is a proxy and not the
255
+ // principle — the principle needs judgement, which COMPLIANCE.md now says
256
+ // rather than claiming every line is mechanically decidable.
257
+ R("S8", "meaning never rests on colour or position alone",
258
+ colourOnly.length === 0,
259
+ colourOnly.length ? `${colourOnly.length} element(s) distinguished only by appearance: ${colourOnly.slice(0, 2).join(", ")}`
260
+ : "no element is distinguished by appearance alone",
261
+ "1.4.1"),
262
+ ]
263
+ }
264
+
265
+ // Runnable on its own, so the checklist is something a team can point at a
266
+ // component rather than something they read about:
267
+ //
268
+ // The library ships this predicate; checks/cli.mjs is the executable entry
269
+ // point that runs it against a page and prints each verdict with its evidence.