Skip to content

Commit dbbc1df

Browse files
authored
Merge pull request #38 from kurtstohrer/feat/ann-16-provider-plugins
feat(ann-16): provider plugins — OpenAI, OpenAICompatible, Copilot, Paperclip
2 parents df25cdd + a58a583 commit dbbc1df

32 files changed

Lines changed: 3179 additions & 98 deletions

docs/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,9 @@ <h1>Annotate the UI. <span class="green">Agent ships the code.</span></h1>
122122
<h2>The loop, in 90 seconds</h2>
123123
<p class="section-lede">One annotation → one structured task → one applied diff → one re-render that passes. No copy-paste from a design tool, no out-of-band agent prompts.</p>
124124
<div class="video-frame">
125-
<img src="media/demo.gif" alt="90-second annotate to apply demo (placeholder)" />
125+
<img src="media/demo.gif" alt="Annotate a low-contrast link, agent applies the a11y fix, task flips to review." />
126126
</div>
127-
<p class="caption">90-second video lands here. Until then, this is the still board for the recorded scenario: <code>a11y_fix</code> on a React + Vite playground.</p>
127+
<p class="caption">Inline loop above. The full 90-second walkthrough — <code>a11y_fix</code> on a React + Vite playground — is being recorded; the link will appear here once it's hosted.</p>
128128
</section>
129129

130130
<section>

docs/media/demo.gif

1.55 MB
Loading

docs/media/hero.png

190 KB
Loading

e2e/react-vite.a11y.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import fs from 'node:fs'
2+
import path from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
import { test, expect } from '@playwright/test'
5+
6+
/**
7+
* Recording-readiness guard for the `a11y_fix` demo on the React + Vite
8+
* playground (ANN-10).
9+
*
10+
* The 90-second demo opens the playground, scans the Hero CTA for a11y
11+
* violations, and lets the agent apply a token-level fix. For that to be
12+
* recording-clean we need two things to hold on `main`:
13+
* 1. The default playground state has exactly one Hero color-contrast hit
14+
* that maps to `var(--accent)` + `var(--text-on-accent)` — so the demo
15+
* has a real failure to scan.
16+
* 2. Darkening `--accent` at the token definition clears the violation
17+
* without touching component CSS — so the agent's first-try token fix
18+
* is mechanically guaranteed to work.
19+
*
20+
* Both checks run against the live playground at port 5174 (see
21+
* `playwright.config.ts`). axe-core is injected directly from
22+
* `node_modules` so we don't need `@axe-core/playwright` as a dep.
23+
*/
24+
25+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
26+
const repoRoot = path.resolve(__dirname, '..')
27+
const axePath = path.join(repoRoot, 'node_modules', 'axe-core', 'axe.min.js')
28+
const axeSource = fs.readFileSync(axePath, 'utf-8')
29+
const tokensPath = path.join(
30+
repoRoot,
31+
'playgrounds',
32+
'simple',
33+
'react-vite',
34+
'src',
35+
'styles',
36+
'tokens.css',
37+
)
38+
39+
interface AxeNode {
40+
target: string[]
41+
html: string
42+
failureSummary: string
43+
any: Array<{ id: string; data?: { fgColor?: string; bgColor?: string; contrastRatio?: number } }>
44+
}
45+
interface AxeRunResult {
46+
violations: Array<{ id: string; nodes: AxeNode[] }>
47+
}
48+
49+
async function runContrast(page: import('@playwright/test').Page): Promise<AxeRunResult> {
50+
await page.evaluate(axeSource)
51+
return await page.evaluate(async () => {
52+
// @ts-ignore - axe is global once injected
53+
return await axe.run(document, {
54+
runOnly: { type: 'rule', values: ['color-contrast'] },
55+
resultTypes: ['violations'],
56+
})
57+
})
58+
}
59+
60+
function heroCtaNodes(result: AxeRunResult): AxeNode[] {
61+
const contrast = result.violations.find(v => v.id === 'color-contrast')
62+
if (!contrast) return []
63+
return contrast.nodes.filter(n => n.html.includes('data-annotask-file="src/components/Hero.tsx"'))
64+
}
65+
66+
test.describe('React + Vite a11y_fix recording readiness', () => {
67+
test('default playground has the Hero CTA color-contrast violation the demo expects', async ({
68+
page,
69+
}) => {
70+
await page.goto('/', { waitUntil: 'networkidle' })
71+
const result = await runContrast(page)
72+
const heroHits = heroCtaNodes(result)
73+
expect(
74+
heroHits.length,
75+
'Hero primary CTA must trigger a color-contrast violation so the demo has something to scan',
76+
).toBeGreaterThan(0)
77+
78+
const cta = heroHits[0]
79+
const contrastData = cta.any.find(a => a.id === 'color-contrast')?.data
80+
expect(contrastData, 'axe must surface fg/bg/ratio for the demo violation').toBeTruthy()
81+
expect(contrastData?.fgColor?.toLowerCase()).toBe('#ffffff')
82+
expect(contrastData?.bgColor?.toLowerCase()).toBe('#007bff')
83+
expect(contrastData?.contrastRatio ?? 99).toBeLessThan(4.5)
84+
})
85+
86+
test('tokens.css resolves the violation when --accent is darkened at the definition', async ({
87+
page,
88+
}) => {
89+
await page.goto('/', { waitUntil: 'networkidle' })
90+
const before = heroCtaNodes(await runContrast(page))
91+
expect(before.length).toBeGreaterThan(0)
92+
93+
// Simulate the agent's first-try token-level fix in-page only — do not
94+
// touch tokens.css on disk, so the recording state on main stays
95+
// "failing by default".
96+
await page.addStyleTag({
97+
content: `:root, :root[data-theme="dark"] { --accent: #0061f2; }`,
98+
})
99+
100+
const after = heroCtaNodes(await runContrast(page))
101+
expect(
102+
after.length,
103+
'Darkening --accent at the token definition must clear the Hero CTA violation',
104+
).toBe(0)
105+
})
106+
107+
test('Hero CTA CSS still references the token (no inline hex override creeps back in)', async () => {
108+
const hero = fs.readFileSync(
109+
path.join(
110+
repoRoot,
111+
'playgrounds',
112+
'simple',
113+
'react-vite',
114+
'src',
115+
'components',
116+
'Hero.module.css',
117+
),
118+
'utf-8',
119+
)
120+
expect(hero).toMatch(/\.primary\s*\{[\s\S]*?background:\s*var\(--accent\)/)
121+
expect(hero).toMatch(/\.primary\s*\{[\s\S]*?color:\s*var\(--text-on-accent\)/)
122+
123+
const tokens = fs.readFileSync(tokensPath, 'utf-8')
124+
// Dark theme default keeps the well-known failing value so the demo has
125+
// a deterministic scan target. If this ever shifts, update the e2e and
126+
// the readiness note together.
127+
expect(tokens).toMatch(/--accent:\s*#007bff/)
128+
})
129+
})

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"node": ">=18"
7979
},
8080
"peerDependencies": {
81+
"@anthropic-ai/sdk": ">=0.40.0",
8182
"@babel/parser": "^7.0.0",
8283
"@vue/compiler-dom": "^3.0.0",
8384
"@vue/compiler-sfc": "^3.0.0",
@@ -86,6 +87,9 @@
8687
"webpack": ">=5.0.0"
8788
},
8889
"peerDependenciesMeta": {
90+
"@anthropic-ai/sdk": {
91+
"optional": true
92+
},
8993
"vite": {
9094
"optional": true
9195
},
@@ -111,6 +115,7 @@
111115
"zod-to-json-schema": "^3.25.2"
112116
},
113117
"devDependencies": {
118+
"@anthropic-ai/sdk": "^0.95.2",
114119
"@babel/parser": "^7.0.0",
115120
"@playwright/test": "^1.58.2",
116121
"@types/js-yaml": "^4.0.9",

pnpm-lock.yaml

Lines changed: 56 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

skills/annotask-apply/A11Y_RULES.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Match on `context.rule`.
3030

3131
| Rule(s) | Fix layer | What to do |
3232
|---|---|---|
33-
| `color-contrast`, `color-contrast-enhanced` | Design tokens first | Read `elements[i].a11y.contrast` for the actual fg/bg hex + ratio. If the colors come from theme tokens, adjust the token. If the contrast comes from a one-off inline style or hardcoded class, fix the component instead of the shared token. |
33+
| `color-contrast`, `color-contrast-enhanced` | Design tokens first | See **Color contrast playbook** below. |
3434
| `label`, `form-field-multiple-labels` | Markup | Wrap input in `<label>` or add `aria-labelledby` that points at visible text. If `name_source === 'placeholder'`, the input still has no real label. |
3535
| `button-name`, `link-name`, `input-button-name` | Markup | Add visible text or `aria-label`. For icon-only buttons, add a visually hidden label (`sr-only` or the project equivalent). Do not rely on `title`. |
3636
| `image-alt`, `role-img-alt`, `svg-img-alt` | Markup | Use `alt=""` for decorative imagery and descriptive `alt` for informational imagery. For SVG icons paired with visible text, use `aria-hidden="true"`. |
@@ -45,6 +45,30 @@ Match on `context.rule`.
4545
| `duplicate-id`, `duplicate-id-active`, `duplicate-id-aria` | Markup | Search the codebase for hardcoded `id="..."` values and rename one. If the id is generated, make the generation key unique per instance. |
4646
| `frame-title`, `iframe-title` | Markup | Add `title="..."` describing the iframe's purpose. |
4747

48+
## Color contrast playbook
49+
50+
Use this when `context.rule` is `color-contrast` or `color-contrast-enhanced`. The goal is to land **one token-level edit** that fixes the violation on the first try, without spraying inline overrides or hardcoded hex into component CSS.
51+
52+
1. **Read the structured contrast block first.** `elements[i].a11y.contrast` gives you `foreground`, `background`, and `ratio` as actual computed hex. Treat these as facts; do not re-derive from screenshots.
53+
2. **Open the source at `elements[i].file:line` and locate the failing CSS rule.** Identify how `color` and `background`/`background-color` are set:
54+
- `var(--token-name)` → token-level fix (preferred).
55+
- hardcoded hex / `rgb()` / `hsl()` → component-level fix.
56+
- inline `style={…}` / `style="…"` → component-level fix on that one instance.
57+
- Tailwind / utility class (e.g. `bg-blue-500 text-white`) → swap utility classes or extend the project's theme config; do not add ad-hoc inline overrides.
58+
3. **For the token path** — the common case in design-system playgrounds:
59+
- Find the variable's definition (typically a `:root` / `:root[data-theme="*"]` block in `tokens.css`, `globals.css`, `theme.css`, or equivalent). Use `grep` for `--token-name:` if you have to.
60+
- Edit the variable **at its definition**, inside the theme block that matches the current `color_scheme` on the task (`dark` vs `light`). Do not redefine the variable on the component selector — that bypasses the design system and breaks every other consumer.
61+
- If both themes look like they fail, fix the variable in each theme block separately, computing per-theme.
62+
4. **Pick which token to move.** When both `color` and `background` are tokens, default to:
63+
- **Brand background + white/near-white text (`--accent` + `--text-on-accent`):** keep the white text token, darken the brand token. Brand tokens earn their lightness; light text on brand is almost always the intended pattern, and darkening the brand by ~10–20% luminance is usually the smallest visible change.
64+
- **Muted text on neutral surface (`--text-muted`, `--text-subtle` on `--surface*`):** lighten the text token, do not darken the surface. Surface tokens drive layout perception and many components consume them.
65+
- **Disabled/placeholder text:** if the project intentionally uses a low-contrast token, keep the rule failure scoped to that pattern and fix only the offending public-facing instance, not the token.
66+
5. **Choose the new value by luminance, not by eye.** For text vs. opaque background, target ≥4.5:1 (normal text) or ≥3:1 (large/bold ≥18pt or 14pt+700). Move the token in **one direction only** (darken bg *or* lighten fg) — do not split the fix across both ends. After editing, the diff should be one line in the tokens file.
67+
6. **Sanity-check sibling tokens.** If you darken `--accent`, glance at `--accent-strong` (the hover/pressed variant) and `--accent-soft` (low-opacity surfaces). If `--accent-strong` is already darker than your new `--accent` value, leave it. If it's now lighter, slide it darker by the same delta so the brand ramp stays monotonic.
68+
7. **Do not touch the component CSS.** A clean token fix has no edits in the component file. If you find yourself opening the component to override the variable, stop — you are about to re-introduce the instance-level pattern the playbook is trying to prevent.
69+
70+
The agent should also confirm the re-render passes after applying: the user's a11y panel re-scans automatically once the file saves, and the violation should drop off. If it doesn't, the wrong token was moved or the change landed in the wrong theme block.
71+
4872
## General rules
4973

5074
- Prefer pattern fixes over instance fixes. If many entries point at the same file or component, fix the source component once.

0 commit comments

Comments
 (0)