From 21a9a9cbb4a875d94d3b5d65068fd5ba0ce9994e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:26:45 -0500 Subject: [PATCH 01/12] feat(contracts): add portable web interaction contract v1 --- contracts/web-interactions.v1.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 contracts/web-interactions.v1.json diff --git a/contracts/web-interactions.v1.json b/contracts/web-interactions.v1.json new file mode 100644 index 0000000..b8df2fe --- /dev/null +++ b/contracts/web-interactions.v1.json @@ -0,0 +1 @@ +{"schemaVersion":"opencoven.ui-web-interactions/v1","contractVersion":"1.0.0","status":"stable","owner":"OpenCoven/ui","scope":"Framework-neutral web semantics and interaction behavior; brand values and product behavior are out of scope.","hooks":{"primitive":"data-oc-primitive","part":"data-oc-part","state":"data-oc-state"},"global":{"nativeFirst":true,"staticFirst":true,"oneFilledAction":true,"targetMinimumCssPx":[44,44],"visibleFocus":true,"nonColorState":true,"modalOnlyFocusTrap":true,"unsupportedIsNotPass":true,"modes":["js","no-js","reduced-motion","forced-colors","320px","200%-zoom"],"states":["idle","hover","focus","pressed","selected","expanded","collapsed","busy","success","error","disabled","unavailable"]},"primitives":{"action":{"selector":"[data-oc-primitive=\"action\"]","semantics":["a[href]=navigation","button=operation"],"parts":["label"],"states":["idle","focus","pressed","busy","success","error","disabled"],"keyboard":["Tab","Enter","Space(button)"],"focus":"natural; never trapped; stays on trigger after async result","noJavaScript":"links work; enhanced-only buttons omitted or have fallback","failure":"visible success/error and recovery; no duplicate activation","forbidden":["clickable div","disabled href","more than one filled action per decision"]},"disclosure":{"selector":"[data-oc-primitive=\"disclosure\"]","semantics":["details/summary or button[aria-expanded][aria-controls]"],"parts":["trigger","panel"],"states":["collapsed","expanded","focus"],"keyboard":["Enter","Space","Escape(enhanced)","Tab"],"focus":"trigger owns open; no trap; Escape restores trigger","noJavaScript":"details works or panel stays visible","failure":"state/ARIA stay synchronized","forbidden":["role=menu for site content","arrow-only use"]},"global-navigation":{"selector":"[data-oc-primitive=\"global-navigation\"]","semantics":["header","nav[aria-label]","ordinary links","aria-current=page"],"parts":["brand-link","primary-links","primary-action"],"states":["idle","focus"],"keyboard":["Tab","Shift+Tab","Enter"],"focus":"reading order; exits both directions","noJavaScript":"all destinations and Start locally are links","failure":"no duplicate hidden nav is focusable","forbidden":["application-menu semantics"]},"mobile-navigation":{"selector":"[data-oc-primitive=\"mobile-navigation\"]","semantics":["details/summary preferred; otherwise disclosure"],"parts":["trigger","panel","links"],"states":["collapsed","expanded","focus"],"keyboard":["Enter","Space","Escape","Tab"],"focus":"not modal; no trap; Escape restores trigger","noJavaScript":"all links remain reachable","failure":"outside click may close; inside link still navigates","forbidden":["dialog semantics","body lock"]},"tabs":{"selector":"[data-oc-primitive=\"tabs\"]","semantics":["tablist","button[role=tab]","tabpanel[aria-labelledby]"],"parts":["list","tab","panel"],"states":["idle","selected","focus","disabled"],"keyboard":["Arrows","Home","End","Enter/Space if manual","Tab"],"focus":"one tab tabindex=0; roving only; no trap","noJavaScript":"all panels visible with headings","failure":"selected tab/panel/ARIA stay synchronized","forbidden":["sequential steps","remote mutation"]},"tooltip":{"selector":"[data-oc-primitive=\"tooltip\"]","semantics":["named focusable trigger","role=tooltip","aria-describedby"],"parts":["trigger","content"],"states":["collapsed","expanded"],"keyboard":["Focus","Escape","Blur"],"focus":"tooltip never receives focus","noJavaScript":"essential explanation is adjacent","failure":"same supplement on focus and hover","forbidden":["interactive content","sole accessible name"]},"status-indicator":{"selector":"[data-oc-primitive=\"status-indicator\"]","semantics":["text; role=status only for meaningful async change"],"parts":["label"],"states":["busy","success","error","unavailable","selected"],"keyboard":[],"focus":"not focusable unless also action","noJavaScript":"wording remains visible","failure":"polite atomic announcement once; unavailable is not zero","forbidden":["color-only dot","routine role=alert"]},"progress":{"selector":"[data-oc-primitive=\"progress\"]","semantics":["progress[value][max] or labelled progressbar; aria-busy for indeterminate"],"parts":["label","value-or-state"],"states":["busy","success","error","unavailable"],"keyboard":[],"focus":"not focusable unless paired with action","noJavaScript":"last verified or explicit unavailable","failure":"no fabricated percent; wording survives reduced motion","forbidden":["decorative/fake progress"]},"copy-control":{"selector":"[data-oc-primitive=\"copy-control\"]","semantics":["button + visible source + polite atomic status"],"parts":["source","trigger","status"],"states":["idle","busy","success","error"],"keyboard":["Enter","Space"],"focus":"stays on trigger","noJavaScript":"source visible/selectable; button optional","failure":"Copied 2–4s or Could not copy; source retained","forbidden":["hidden source","copying secrets"]},"download-chooser":{"selector":"[data-oc-primitive=\"download-chooser\"]","semantics":["ordinary artifact links; optional native recommendation control"],"parts":["artifact-links","fallback"],"states":["idle","selected","error","unavailable"],"keyboard":["Tab","Enter","native select/radio"],"focus":"no trap","noJavaScript":"all artifacts and releases fallback visible","failure":"resolver failure reaches direct fallback; page JS never owns installer bytes","forbidden":["fetch/blob installer","auto-download","hidden alternatives"]},"theme-control":{"selector":"[data-oc-primitive=\"theme-control\"]","semantics":["fieldset/radios or select with system/light/dark"],"parts":["label","control"],"states":["selected","focus"],"keyboard":["native control keys"],"focus":"natural","noJavaScript":"system remains valid","failure":"store only system/light/dark; pre-paint apply; system tracks OS","forbidden":["ambiguous two-state toggle","animated page flash"]},"dialog":{"selector":"[data-oc-primitive=\"dialog\"]","semantics":["dialog.showModal; aria-modal only for true fallback"],"parts":["title","content","close"],"states":["collapsed","expanded","focus"],"keyboard":["Enter/Space(open)","Tab/Shift+Tab(contained)","Escape"],"focus":"initial inside; trap only while modal; every close restores opener","noJavaScript":"content inline or normal destination","failure":"background inert; labelled; close always available","forbidden":["mobile nav dialog","nested modal","lost opener"]},"guided-proof":{"selector":"[data-oc-primitive=\"guided-proof\"]","semantics":["tabs or native radios + semantic static states"],"parts":["controls","state-label","evidence-region"],"states":["selected","focus","unavailable"],"keyboard":["native tabs/radios"],"focus":"no trap","noJavaScript":"all three states sequentially readable","failure":"deterministic; meaning independent of canvas/WebGL/motion","forbidden":["fake app","drag-only 3D"]},"error-surface":{"selector":"[data-oc-primitive=\"error-surface\"]","semantics":["section/aside heading; alert only urgent new error; status for routine async"],"parts":["title","description","recovery-or-fallback"],"states":["busy","success","error","unavailable"],"keyboard":["normal action keys"],"focus":"routine update never steals focus","noJavaScript":"fallback remains","failure":"recovery/next step visible; no endless retry","forbidden":["blank spinner","alert for every load"]}}} From a35b0d80683eed596e888b16f62cc75b3ff15dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:27:16 -0500 Subject: [PATCH 02/12] test(contracts): add shared portable interaction vectors --- contracts/test-vectors.v1.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 contracts/test-vectors.v1.json diff --git a/contracts/test-vectors.v1.json b/contracts/test-vectors.v1.json new file mode 100644 index 0000000..e5263a2 --- /dev/null +++ b/contracts/test-vectors.v1.json @@ -0,0 +1 @@ +{"schemaVersion":"opencoven.ui-test-vectors/v1","contractVersion":"1.0.0","result":"pass|fail|unsupported-with-reason; unsupported is never pass","vectors":[{"id":"NAV-001","primitive":"global-navigation","modes":["js","no-js"],"steps":["inspect landmarks/links"],"assertions":["one named global nav","brand and Start locally are links","one aria-current when represented","no duplicate focusable nav"],"mutationGuard":"remove label or expose duplicate"},{"id":"NAV-002","primitive":"global-navigation","modes":["js","no-js"],"steps":["Tab forward/back","Enter links"],"assertions":["visible focus","reading order","focus exits both directions","href followed"],"mutationGuard":"preventDefault Tab"},{"id":"MOB-001","primitive":"mobile-navigation","modes":["js"],"steps":["open","Tab inside","Escape"],"assertions":["44×44 trigger","open state synced","not modal/inert","Escape closes and restores trigger"],"mutationGuard":"leave expanded or lose focus"},{"id":"MOB-002","primitive":"mobile-navigation","modes":["no-js"],"steps":["open details if used","traverse links"],"assertions":["all destinations present","Start locally present","no script overlay"],"mutationGuard":"hide panel pending JS"},{"id":"MOB-003","primitive":"mobile-navigation","modes":["js"],"steps":["open","outside click","reopen","activate link"],"assertions":["outside closes","inside link navigates once"],"mutationGuard":"backdrop swallows link"},{"id":"ACT-001","primitive":"action","modes":["js","no-js","320px"],"steps":["inspect/activate each"],"assertions":["a for navigation","button for operation","44×44 targets","one filled action"],"mutationGuard":"clickable div or two filled actions"},{"id":"ACT-002","primitive":"action","modes":["js"],"steps":["activate async twice"],"assertions":["one operation","name retained","busy exposed","visible result/recovery"],"mutationGuard":"duplicate request or blank busy label"},{"id":"THEME-001","primitive":"theme-control","modes":["js"],"steps":["select light/dark/system","reload","change OS"],"assertions":["only three stored values","explicit survives","system tracks OS","pre-paint state matches preference"],"mutationGuard":"two-state toggle"},{"id":"THEME-002","primitive":"theme-control","modes":["no-js","forced-colors"],"steps":["load light/dark/high contrast"],"assertions":["system remains legible","content/navigation complete"],"mutationGuard":"hard-code unusable theme"},{"id":"COPY-001","primitive":"copy-control","modes":["js"],"steps":["grant clipboard","activate"],"assertions":["source equals copy","focus retained","Copied visible 2–4s","one polite announcement"],"mutationGuard":"move focus to toast"},{"id":"COPY-002","primitive":"copy-control","modes":["js","no-js"],"steps":["reject clipboard or disable JS"],"assertions":["failure wording","source selectable","no false success","static source visible"],"mutationGuard":"hide source"},{"id":"TABS-001","primitive":"tabs","modes":["js"],"steps":["ArrowRight","End","Home","Tab"],"assertions":["one selected/tabindex0","keys select correctly","matching panel","Tab exits widget"],"mutationGuard":"all tabs tabindex0"},{"id":"TABS-002","primitive":"tabs","modes":["no-js"],"steps":["inspect source order"],"assertions":["every panel visible","each panel labelled"],"mutationGuard":"ship inactive panels hidden"},{"id":"TIP-001","primitive":"tooltip","modes":["js"],"steps":["focus/Escape","hover/leave"],"assertions":["same supplement","tooltip not focusable","control already named","essential text outside"],"mutationGuard":"put link inside tooltip"},{"id":"STATUS-001","primitive":"status-indicator","modes":["js","no-js","forced-colors"],"steps":["inspect static/dynamic"],"assertions":["visible wording/non-color cue","routine update polite once","unavailable not zero"],"mutationGuard":"colored dot only"},{"id":"PROGRESS-001","primitive":"progress","modes":["js","reduced-motion"],"steps":["inspect determinate/indeterminate/unavailable"],"assertions":["valid measured value","busy wording","no fabricated percent","motion not required"],"mutationGuard":"hard-code fake 73%"},{"id":"DOWN-001","primitive":"download-chooser","modes":["js","no-js"],"steps":["traverse artifacts/fallback"],"assertions":["ordinary hrefs","all alternatives","activation required","page does not fetch bytes"],"mutationGuard":"fetch/blob installer"},{"id":"DOWN-002","primitive":"download-chooser","modes":["js"],"steps":["fail resolver/stream"],"assertions":["direct release fallback or error surface","no endless retry"],"mutationGuard":"swallow failure"},{"id":"DIALOG-001","primitive":"dialog","modes":["js"],"steps":["open","Tab cycle","Escape"],"assertions":["labelled","initial focus inside","focus contained","background inert","focus restored"],"mutationGuard":"restore to body"},{"id":"DIALOG-002","primitive":"dialog","modes":["no-js"],"steps":["inspect fallback"],"assertions":["content inline or ordinary link","nothing essential modal-only"],"mutationGuard":"hide all dialog content"},{"id":"PROOF-001","primitive":"guided-proof","modes":["js","reduced-motion","320px"],"steps":["select three states"],"assertions":["three named states","non-color selection","labelled evidence","no remote mutation","no WebGL requirement"],"mutationGuard":"canvas-only meaning"},{"id":"PROOF-002","primitive":"guided-proof","modes":["no-js"],"steps":["read sequence"],"assertions":["all three visible","headings/evidence for each"],"mutationGuard":"render first only"},{"id":"ERROR-001","primitive":"error-surface","modes":["js","no-js"],"steps":["render busy/error/unavailable","traverse recovery"],"assertions":["heading/description","fallback/next step","routine update keeps focus","recovery follows action"],"mutationGuard":"blank spinner"},{"id":"REFLOW-001","primitive":"action","modes":["320px","200%-zoom","forced-colors"],"steps":["traverse all controls"],"assertions":["44×44 targets","no overlap","no two-dimensional scroll","focus not obscured","system focus visible"],"mutationGuard":"shrink or hide focused target"}]} From 539f0858fbc27ab2c9a9f200c68c73d6a5cb4cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:28:52 -0500 Subject: [PATCH 03/12] docs(contracts): document portable web behavior and conformance --- contracts/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 contracts/README.md diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 0000000..929ec70 --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,19 @@ +# Portable web interaction contract v1.0.0 + +`web-interactions.v1.json` is the framework-neutral semantic and interaction contract owned by `OpenCoven/ui`. `test-vectors.v1.json` is the shared conformance suite. `fixtures/reference.html` demonstrates the native-first baseline without importing React, Tailwind, Base UI, or the specimen app. + +Visual identity and token values remain owned by `OpenCoven/brand`. Product state, authority, protocol, and runtime behavior remain owned by the corresponding product repositories. + +## Stable hooks + +Consumers expose `data-oc-primitive`, `data-oc-part`, and optional `data-oc-state` hooks named by the contract. These are testing semantics, not a styling API, and never replace native attributes such as `href`, `open`, `disabled`, `aria-expanded`, `aria-selected`, or `aria-busy`. + +## Conformance + +Run every applicable vector with JavaScript enabled and disabled, keyboard-only input, 320 px width, 200% zoom/reflow, reduced motion, and forced colors where supported. Each vector returns `pass`, `fail`, or `unsupported-with-reason`; unsupported is never counted as pass. + +Ordinary navigation is not an application menu. Mobile navigation is not a modal. Tabs do not perform remote mutation. Tooltips contain no essential or interactive content. Progress is measured, not decorative. Download links remain browser-native; page JavaScript never owns installer bytes. Dialogs are reserved for true modal tasks. Guided proof remains semantic and complete without canvas, WebGL, drag, or motion. + +## Versioning + +Patch changes clarify wording without changing behavior. Minor changes add backward-compatible primitives, parts, states, or vectors. Major changes remove/rename hooks or alter required keyboard, focus, failure, or progressive-enhancement behavior. Consumers pin an immutable revision and run the shared vectors before updating. From 25f65bce498aacd68e4da404d147d9ca70062c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:29:02 -0500 Subject: [PATCH 04/12] docs(contracts): record portable contract v1 release --- contracts/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 contracts/CHANGELOG.md diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md new file mode 100644 index 0000000..6924119 --- /dev/null +++ b/contracts/CHANGELOG.md @@ -0,0 +1,8 @@ +# Portable interaction contract changelog + +## 1.0.0 — 2026-08-30 + +- Added 14 framework-neutral web primitives. +- Added stable conformance hooks and shared state vocabulary. +- Added 24 mutation-guarded keyboard, focus, no-JavaScript, failure, reduced-motion, forced-colors, target-size, and reflow vectors. +- Added a native-first reference fixture. From 40f97f1e4151ec8f31065e354437dcc2d55a5f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:29:38 -0500 Subject: [PATCH 05/12] feat(contracts): add native portable interaction fixture --- contracts/fixtures/reference.html | 1 + 1 file changed, 1 insertion(+) create mode 100644 contracts/fixtures/reference.html diff --git a/contracts/fixtures/reference.html b/contracts/fixtures/reference.html new file mode 100644 index 0000000..36b8929 --- /dev/null +++ b/contracts/fixtures/reference.html @@ -0,0 +1 @@ +OpenCoven portable interaction reference
OpenCoven
Menu

Portable interaction reference

Actions

Start locally Understand the protocol

Theme

Copy

npm install -g @opencoven/cli

Views

Overview

Visible without JavaScript.

Evidence

Also visible without JavaScript.

Help

Essential explanation is already visible.

Status

✓ Verified from a pinned revision

Progress

2 of 3

Download

All release artifacts

Identity → authority → execution

Inspect a state

All three states remain below.

Identity bound

A familiar revision is named.

Authority checked

Protected change is permitted, degraded, or rejected.

Execution receipted

The runtime records what happened.

Dialog

Read inline or

Modal reference

A true modal contains and restores focus.

Inline fallback

Available without JavaScript.

Evidence unavailable

No verified value was returned, so no zero is shown.

Inspect source

From 8cce7d2c7a0324704ba48feac2960e9925f39515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:30:01 -0500 Subject: [PATCH 06/12] feat(contracts): implement reference keyboard and failure behavior --- contracts/fixtures/reference.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 contracts/fixtures/reference.js diff --git a/contracts/fixtures/reference.js b/contracts/fixtures/reference.js new file mode 100644 index 0000000..50864e9 --- /dev/null +++ b/contracts/fixtures/reference.js @@ -0,0 +1,11 @@ +document.documentElement.classList.add("enhanced"); +const mobile=document.querySelector('[data-oc-primitive="mobile-navigation"]'); +if(mobile instanceof HTMLDetailsElement){const trigger=mobile.querySelector('[data-oc-part="trigger"]');const sync=()=>mobile.dataset.ocState=mobile.open?"expanded":"collapsed";mobile.addEventListener("toggle",sync);mobile.addEventListener("keydown",event=>{if(event.key==="Escape"&&mobile.open){mobile.open=false;trigger?.focus()}});document.addEventListener("pointerdown",event=>{if(mobile.open&&!mobile.contains(event.target))mobile.open=false});sync()} +const theme=document.querySelector('[data-oc-primitive="theme-control"] select'); +if(theme instanceof HTMLSelectElement){const allowed=new Set(["system","light","dark"]);const stored=localStorage.getItem("oc-theme");theme.value=allowed.has(stored)?stored:"system";const apply=()=>{const value=allowed.has(theme.value)?theme.value:"system";document.documentElement.dataset.theme=value;localStorage.setItem("oc-theme",value)};theme.addEventListener("change",apply);apply()} +const copy=document.querySelector('[data-oc-primitive="copy-control"]'); +if(copy){const source=copy.querySelector('[data-oc-part="source"]');const trigger=copy.querySelector('[data-oc-part="trigger"]');const status=copy.querySelector('[data-oc-part="status"]');if(trigger instanceof HTMLButtonElement&&source&&status){trigger.hidden=false;trigger.addEventListener("click",async()=>{trigger.disabled=true;try{await navigator.clipboard.writeText(source.textContent??"");trigger.textContent="Copied";status.textContent="Command copied.";setTimeout(()=>{trigger.textContent="Copy";status.textContent="";trigger.disabled=false},2500)}catch{trigger.textContent="Copy";status.textContent="Could not copy. Select manually.";trigger.disabled=false}})}} +for(const tabs of document.querySelectorAll('[data-oc-primitive="tabs"]')){const list=[...tabs.querySelectorAll('[role="tab"]')];const panels=[...tabs.querySelectorAll('[role="tabpanel"]')];const select=(next,focus=true)=>{for(const tab of list){const selected=tab===next;tab.setAttribute("aria-selected",String(selected));tab.tabIndex=selected?0:-1;const panel=panels.find(item=>item.id===tab.getAttribute("aria-controls"));if(panel)panel.hidden=!selected}if(focus)next.focus()};list.forEach((tab,index)=>{tab.addEventListener("click",()=>select(tab,false));tab.addEventListener("keydown",event=>{let next=null;if(event.key==="ArrowRight")next=(index+1)%list.length;if(event.key==="ArrowLeft")next=(index-1+list.length)%list.length;if(event.key==="Home")next=0;if(event.key==="End")next=list.length-1;if(next!==null){event.preventDefault();select(list[next])}})});select(list.find(tab=>tab.getAttribute("aria-selected")==="true")??list[0],false)} +for(const surface of document.querySelectorAll('[data-oc-primitive="tooltip"]')){const trigger=surface.querySelector('[data-oc-part="trigger"]');const content=surface.querySelector('[data-oc-part="content"]');if(!trigger||!content)continue;const show=()=>content.hidden=false;const hide=()=>content.hidden=true;trigger.addEventListener("focus",show);trigger.addEventListener("blur",hide);trigger.addEventListener("pointerenter",show);trigger.addEventListener("pointerleave",hide);trigger.addEventListener("keydown",event=>{if(event.key==="Escape")hide()})} +const dialog=document.querySelector('[data-oc-primitive="dialog"]');const open=document.querySelector("[data-dialog-open]"); +if(dialog instanceof HTMLDialogElement&&open instanceof HTMLButtonElement){const title=dialog.querySelector('[data-oc-part="title"]');const close=dialog.querySelector('[data-oc-part="close"]');let opener=null;open.addEventListener("click",()=>{opener=open;dialog.showModal();title?.focus()});close?.addEventListener("click",()=>dialog.close());dialog.addEventListener("close",()=>opener?.focus())} From 22c9fc7819727466577a6df2fe1f61ed8c9c335d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:30:13 -0500 Subject: [PATCH 07/12] chore(contracts): keep immutable machine artifacts formatter-neutral --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index cd48a03..2178fb9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,4 @@ registry.json pnpm-lock.yaml index.html Components.dc.html +contracts From f87439787421b161c112da3807b437a3dfc43a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:32:33 -0500 Subject: [PATCH 08/12] test(contracts): validate portable primitives, vectors, and fixture --- scripts/verify-contracts.mjs | 164 ++++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 3 deletions(-) diff --git a/scripts/verify-contracts.mjs b/scripts/verify-contracts.mjs index 4b19463..c6c3a47 100644 --- a/scripts/verify-contracts.mjs +++ b/scripts/verify-contracts.mjs @@ -12,6 +12,11 @@ const [ button, tooltip, menu, + portableJson, + vectorsJson, + fixture, + fixtureScript, + portableDocs, ] = await Promise.all([ read("components.json"), read("packages/ui/package.json"), @@ -20,10 +25,17 @@ const [ read("packages/ui/src/components/ui/button.tsx"), read("packages/ui/src/components/ui/tooltip.tsx"), read("packages/ui/src/components/ui/dropdown-menu.tsx"), + read("contracts/web-interactions.v1.json"), + read("contracts/test-vectors.v1.json"), + read("contracts/fixtures/reference.html"), + read("contracts/fixtures/reference.js"), + read("contracts/README.md"), ]); const config = JSON.parse(componentsJson); const manifest = JSON.parse(packageJson); +const portable = JSON.parse(portableJson); +const vectors = JSON.parse(vectorsJson); const assertions = [ ["style is base-nova", config.style === "base-nova"], ["base color is zinc", config.tailwind.baseColor === "zinc"], @@ -91,11 +103,157 @@ const assertions = [ ], ]; -const failed = assertions.filter(([, passed]) => !passed); +const requiredPrimitives = [ + "action", + "disclosure", + "global-navigation", + "mobile-navigation", + "tabs", + "tooltip", + "status-indicator", + "progress", + "copy-control", + "download-chooser", + "theme-control", + "dialog", + "guided-proof", + "error-surface", +]; +const portableFailures = []; +const requirePortable = (condition, message) => { + if (!condition) portableFailures.push(message); +}; + +requirePortable( + portable.schemaVersion === "opencoven.ui-web-interactions/v1", + "portable schemaVersion must be v1", +); +requirePortable( + vectors.schemaVersion === "opencoven.ui-test-vectors/v1", + "vector schemaVersion must be v1", +); +requirePortable( + portable.contractVersion === vectors.contractVersion && + /^\d+\.\d+\.\d+$/.test(portable.contractVersion), + "portable contract and vector semver must match", +); +requirePortable(portable.owner === "OpenCoven/ui", "portable owner must be OpenCoven/ui"); +for (const law of [ + "nativeFirst", + "staticFirst", + "oneFilledAction", + "visibleFocus", + "nonColorState", + "modalOnlyFocusTrap", + "unsupportedIsNotPass", +]) { + requirePortable(portable.global?.[law] === true, `missing global law: ${law}`); +} +requirePortable( + portable.global?.targetMinimumCssPx?.every((value) => value >= 44), + "target minimum must be 44×44 CSS px", +); + +const allowedStates = new Set(portable.global?.states ?? []); +for (const id of requiredPrimitives) { + const primitive = portable.primitives?.[id]; + requirePortable(Boolean(primitive), `missing primitive: ${id}`); + if (!primitive) continue; + requirePortable( + primitive.selector === `[data-oc-primitive="${id}"]`, + `${id} selector drifted`, + ); + for (const field of [ + "semantics", + "parts", + "states", + "keyboard", + "focus", + "noJavaScript", + "failure", + "forbidden", + ]) { + requirePortable(primitive[field] !== undefined, `${id} missing ${field}`); + } + for (const state of primitive.states ?? []) { + requirePortable(allowedStates.has(state), `${id} has unknown state ${state}`); + } +} + +const vectorIds = new Set(); +const covered = new Set(); +for (const vector of vectors.vectors ?? []) { + requirePortable(!vectorIds.has(vector.id), `duplicate vector ${vector.id}`); + vectorIds.add(vector.id); + covered.add(vector.primitive); + requirePortable( + Boolean(portable.primitives?.[vector.primitive]), + `${vector.id} has unknown primitive`, + ); + for (const field of ["modes", "steps", "assertions", "mutationGuard"]) { + requirePortable(vector[field] !== undefined, `${vector.id} missing ${field}`); + } +} +requirePortable(vectorIds.size >= 24, "at least 24 shared vectors are required"); +for (const id of requiredPrimitives.filter((id) => id !== "disclosure")) { + requirePortable(covered.has(id), `no vector covers ${id}`); +} +for (const mode of ["no-js", "reduced-motion", "forced-colors", "320px", "200%-zoom"]) { + requirePortable( + vectors.vectors.some((vector) => vector.modes.includes(mode)), + `no vector covers ${mode}`, + ); +} + +for (const id of requiredPrimitives.filter((id) => id !== "disclosure")) { + requirePortable( + fixture.includes(`data-oc-primitive="${id}"`), + `reference fixture is missing ${id}`, + ); +} +requirePortable( + fixture.includes(" !passed).map(([name]) => name), + ...portableFailures, +]; if (failed.length > 0) { throw new Error( - `Contract verification failed:\n${failed.map(([name]) => `- ${name}`).join("\n")}`, + `Contract verification failed:\n${failed.map((name) => `- ${name}`).join("\n")}`, ); } -console.log(`Verified ${assertions.length} architecture and design contracts.`); +console.log( + `Verified ${assertions.length} architecture contracts and ${requiredPrimitives.length} portable primitives across ${vectorIds.size} shared vectors.`, +); From b71eb173c3989947618d6b2197e5b9c966578111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:33:05 -0500 Subject: [PATCH 09/12] chore(contracts): lint portable fixture with browser globals --- eslint.config.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/eslint.config.js b/eslint.config.js index 64553a8..9368801 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -14,6 +14,12 @@ export default defineConfig([ globals: globals.node, }, }, + { + files: ["contracts/fixtures/**/*.js"], + languageOptions: { + globals: globals.browser, + }, + }, { files: ["**/*.{ts,tsx}"], extends: [ From 692d666ac06020ac81889621a59102a1e7536ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:36:43 -0500 Subject: [PATCH 10/12] ci(debug): print portable verifier formatter diff --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff838e8..47f0816 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,4 +22,9 @@ jobs: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile + - name: Print formatter diff for portable verifier + run: | + pnpm exec prettier --write scripts/verify-contracts.mjs + git diff -- scripts/verify-contracts.mjs + exit 1 - run: pnpm check From 746195003e3907b380a32965d3fc49949d90c29e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:38:47 -0500 Subject: [PATCH 11/12] style(contracts): apply workspace formatter to portable verifier --- scripts/verify-contracts.mjs | 39 ++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/scripts/verify-contracts.mjs b/scripts/verify-contracts.mjs index c6c3a47..87d495c 100644 --- a/scripts/verify-contracts.mjs +++ b/scripts/verify-contracts.mjs @@ -137,7 +137,10 @@ requirePortable( /^\d+\.\d+\.\d+$/.test(portable.contractVersion), "portable contract and vector semver must match", ); -requirePortable(portable.owner === "OpenCoven/ui", "portable owner must be OpenCoven/ui"); +requirePortable( + portable.owner === "OpenCoven/ui", + "portable owner must be OpenCoven/ui", +); for (const law of [ "nativeFirst", "staticFirst", @@ -147,7 +150,10 @@ for (const law of [ "modalOnlyFocusTrap", "unsupportedIsNotPass", ]) { - requirePortable(portable.global?.[law] === true, `missing global law: ${law}`); + requirePortable( + portable.global?.[law] === true, + `missing global law: ${law}`, + ); } requirePortable( portable.global?.targetMinimumCssPx?.every((value) => value >= 44), @@ -176,7 +182,10 @@ for (const id of requiredPrimitives) { requirePortable(primitive[field] !== undefined, `${id} missing ${field}`); } for (const state of primitive.states ?? []) { - requirePortable(allowedStates.has(state), `${id} has unknown state ${state}`); + requirePortable( + allowedStates.has(state), + `${id} has unknown state ${state}`, + ); } } @@ -191,14 +200,26 @@ for (const vector of vectors.vectors ?? []) { `${vector.id} has unknown primitive`, ); for (const field of ["modes", "steps", "assertions", "mutationGuard"]) { - requirePortable(vector[field] !== undefined, `${vector.id} missing ${field}`); + requirePortable( + vector[field] !== undefined, + `${vector.id} missing ${field}`, + ); } } -requirePortable(vectorIds.size >= 24, "at least 24 shared vectors are required"); +requirePortable( + vectorIds.size >= 24, + "at least 24 shared vectors are required", +); for (const id of requiredPrimitives.filter((id) => id !== "disclosure")) { requirePortable(covered.has(id), `no vector covers ${id}`); } -for (const mode of ["no-js", "reduced-motion", "forced-colors", "320px", "200%-zoom"]) { +for (const mode of [ + "no-js", + "reduced-motion", + "forced-colors", + "320px", + "200%-zoom", +]) { requirePortable( vectors.vectors.some((vector) => vector.modes.includes(mode)), `no vector covers ${mode}`, @@ -216,11 +237,13 @@ requirePortable( "mobile reference must be static-first", ); requirePortable( - fixture.includes('aria-live="polite"') && fixture.includes('aria-atomic="true"'), + fixture.includes('aria-live="polite"') && + fixture.includes('aria-atomic="true"'), "async reference status must be polite and atomic", ); requirePortable( - fixture.includes('href="/download/mac"') && fixture.includes("releases/latest"), + fixture.includes('href="/download/mac"') && + fixture.includes("releases/latest"), "download reference must retain links and fallback", ); requirePortable( From 9971b68db1a13e1d7b6b6758ff3d18aa3e329177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Sun, 30 Aug 2026 21:38:56 -0500 Subject: [PATCH 12/12] ci: restore full portable contract gate --- .github/workflows/ci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47f0816..ff838e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,4 @@ jobs: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - - name: Print formatter diff for portable verifier - run: | - pnpm exec prettier --write scripts/verify-contracts.mjs - git diff -- scripts/verify-contracts.mjs - exit 1 - run: pnpm check