ctx-cc 3.5.0 → 4.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/README.md +375 -676
- package/agents/ctx-arch-mapper.md +5 -3
- package/agents/ctx-auditor.md +5 -3
- package/agents/ctx-codex-reviewer.md +214 -0
- package/agents/ctx-concerns-mapper.md +5 -3
- package/agents/ctx-criteria-suggester.md +6 -4
- package/agents/ctx-debugger.md +5 -3
- package/agents/ctx-designer.md +488 -114
- package/agents/ctx-discusser.md +5 -3
- package/agents/ctx-executor.md +5 -3
- package/agents/ctx-handoff.md +6 -4
- package/agents/ctx-learner.md +5 -3
- package/agents/ctx-mapper.md +4 -3
- package/agents/ctx-ml-analyst.md +600 -0
- package/agents/ctx-ml-engineer.md +933 -0
- package/agents/ctx-ml-reviewer.md +485 -0
- package/agents/ctx-ml-scientist.md +626 -0
- package/agents/ctx-parallelizer.md +4 -3
- package/agents/ctx-planner.md +5 -3
- package/agents/ctx-predictor.md +4 -3
- package/agents/ctx-qa.md +5 -3
- package/agents/ctx-quality-mapper.md +5 -3
- package/agents/ctx-researcher.md +5 -3
- package/agents/ctx-reviewer.md +6 -4
- package/agents/ctx-team-coordinator.md +5 -3
- package/agents/ctx-tech-mapper.md +5 -3
- package/agents/ctx-verifier.md +5 -3
- package/bin/ctx.js +199 -27
- package/commands/brand.md +309 -0
- package/commands/ctx.md +10 -10
- package/commands/design.md +304 -0
- package/commands/experiment.md +251 -0
- package/commands/help.md +57 -7
- package/commands/init.md +25 -0
- package/commands/metrics.md +1 -1
- package/commands/milestone.md +1 -1
- package/commands/ml-status.md +197 -0
- package/commands/monitor.md +1 -1
- package/commands/train.md +266 -0
- package/commands/visual-qa.md +559 -0
- package/commands/voice.md +1 -1
- package/hooks/post-tool-use.js +39 -0
- package/hooks/pre-tool-use.js +94 -0
- package/hooks/subagent-stop.js +32 -0
- package/package.json +9 -3
- package/plugin.json +46 -0
- package/skills/ctx-design-system/SKILL.md +572 -0
- package/skills/ctx-ml-experiment/SKILL.md +334 -0
- package/skills/ctx-ml-pipeline/SKILL.md +437 -0
- package/skills/ctx-orchestrator/SKILL.md +91 -0
- package/skills/ctx-review-gate/SKILL.md +147 -0
- package/skills/ctx-state/SKILL.md +100 -0
- package/skills/ctx-visual-qa/SKILL.md +587 -0
- package/src/agents.js +109 -0
- package/src/auto.js +287 -0
- package/src/capabilities.js +226 -0
- package/src/commits.js +94 -0
- package/src/config.js +112 -0
- package/src/context.js +241 -0
- package/src/handoff.js +156 -0
- package/src/hooks.js +218 -0
- package/src/install.js +125 -50
- package/src/lifecycle.js +194 -0
- package/src/metrics.js +198 -0
- package/src/pipeline.js +269 -0
- package/src/review-gate.js +338 -0
- package/src/runner.js +120 -0
- package/src/skills.js +143 -0
- package/src/state.js +267 -0
- package/src/worktree.js +244 -0
- package/templates/PRD.json +1 -1
- package/templates/config.json +4 -237
- package/workflows/ctx-router.md +0 -485
- package/workflows/map-codebase.md +0 -329
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ctx-state
|
|
3
|
+
description: |
|
|
4
|
+
WHEN: Any CTX operation needs to read or write project state (.ctx/STATE.json), track phase transitions, log agent invocations, or record completed tasks.
|
|
5
|
+
WHEN NOT: Non-CTX work, or when the user hasn't initialized a CTX project.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# CTX State Manager
|
|
9
|
+
|
|
10
|
+
Manages persistent state in `.ctx/STATE.json` for phase tracking, agent history, and task completion.
|
|
11
|
+
|
|
12
|
+
## State Schema
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"version": "4.0",
|
|
17
|
+
"phase": "init|plan|execute|verify|complete",
|
|
18
|
+
"activeStory": "S001",
|
|
19
|
+
"storyTitle": "Story title",
|
|
20
|
+
"completedTasks": [
|
|
21
|
+
{ "taskId": "T1", "title": "...", "completedAt": "ISO", "criteriaSatisfied": ["AC1"] }
|
|
22
|
+
],
|
|
23
|
+
"agentHistory": [
|
|
24
|
+
{ "agent": "ctx-planner", "invokedAt": "ISO", "completedAt": "ISO", "taskSummary": "..." }
|
|
25
|
+
],
|
|
26
|
+
"reviewGate": {
|
|
27
|
+
"cycle": 0,
|
|
28
|
+
"history": []
|
|
29
|
+
},
|
|
30
|
+
"pipeline": null,
|
|
31
|
+
"session": {
|
|
32
|
+
"startedAt": "ISO",
|
|
33
|
+
"lastActivity": "ISO"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Valid Phase Transitions
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
init → plan
|
|
42
|
+
plan → execute
|
|
43
|
+
execute → verify
|
|
44
|
+
verify → complete (if pass)
|
|
45
|
+
verify → execute (if fail — retry)
|
|
46
|
+
complete → init (next story)
|
|
47
|
+
ANY → init (reset)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**All other transitions are INVALID.** If you need to skip phases, reset to init first.
|
|
51
|
+
|
|
52
|
+
## Operations
|
|
53
|
+
|
|
54
|
+
### Initialize
|
|
55
|
+
Create `.ctx/STATE.json` with `phase: "init"`. Optionally set `activeStory` and `storyTitle`.
|
|
56
|
+
|
|
57
|
+
### Transition Phase
|
|
58
|
+
1. Read current state
|
|
59
|
+
2. Validate transition (see table above)
|
|
60
|
+
3. Update `phase` field
|
|
61
|
+
4. Update `session.lastActivity`
|
|
62
|
+
5. Write state
|
|
63
|
+
|
|
64
|
+
### Log Agent Invocation
|
|
65
|
+
Append to `agentHistory`:
|
|
66
|
+
```json
|
|
67
|
+
{ "agent": "<name>", "invokedAt": "<now>", "completedAt": null, "taskSummary": "<what>" }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Complete Agent Invocation
|
|
71
|
+
Find last incomplete entry for the agent, set `completedAt` to now.
|
|
72
|
+
|
|
73
|
+
### Record Completed Task
|
|
74
|
+
Append to `completedTasks`:
|
|
75
|
+
```json
|
|
76
|
+
{ "taskId": "<id>", "title": "<title>", "completedAt": "<now>", "criteriaSatisfied": ["AC1"] }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Session Handoff (Pause/Resume)
|
|
80
|
+
**Pause:** Write `.ctx/HANDOFF.json` with current state snapshot + summary + next action.
|
|
81
|
+
**Resume:** Read HANDOFF.json, archive it to `.ctx/handoffs/`, display summary.
|
|
82
|
+
|
|
83
|
+
## Context Profiles
|
|
84
|
+
|
|
85
|
+
Different agents need different project context. Load only what's needed:
|
|
86
|
+
|
|
87
|
+
| Agent Type | Context Sources |
|
|
88
|
+
|------------|----------------|
|
|
89
|
+
| Planning (planner, predictor, criteria) | state, prd, repoMap, readme |
|
|
90
|
+
| Execution (executor, debugger) | state, plan, repoMap |
|
|
91
|
+
| Review (reviewer, auditor, verifier) | state, gitDiff, prd |
|
|
92
|
+
| Mapping (mapper, arch-map, tech-map) | fileTree, readme |
|
|
93
|
+
| Coordination (handoff, team) | state only |
|
|
94
|
+
|
|
95
|
+
## Rules
|
|
96
|
+
|
|
97
|
+
- ALWAYS read state before modifying it (no blind writes)
|
|
98
|
+
- ALWAYS validate transitions before applying them
|
|
99
|
+
- NEVER modify state from review/mapper agents — they are read-only
|
|
100
|
+
- State file is the single source of truth for CTX workflow position
|
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ctx-visual-qa
|
|
3
|
+
description: |
|
|
4
|
+
WHEN: Visual QA, design parity check, pixel-perfect verification, responsive testing across breakpoints, accessibility audit (WCAG 2.2 AA), visual regression after design changes.
|
|
5
|
+
WHEN NOT: Functional testing unrelated to visuals, API testing, backend bug fixes, non-visual logic bugs.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# CTX Visual QA — Measurement-Driven Design Parity
|
|
9
|
+
|
|
10
|
+
You verify that rendered output matches design specifications using numerical comparison, not subjective visual judgment. Every delta is measured and reported as a number. Fixes are specific: "change `font-size` from `14px` to `16px`".
|
|
11
|
+
|
|
12
|
+
## Core Principle: Measure, Don't Guess
|
|
13
|
+
|
|
14
|
+
Visual QA is not screenshot comparison. It is numerical audit:
|
|
15
|
+
|
|
16
|
+
1. **Extract specs** from Figma via MCP — the design source of truth
|
|
17
|
+
2. **Measure rendered output** via Playwright `browser_evaluate` using `getComputedStyle` and `getBoundingClientRect`
|
|
18
|
+
3. **Compute deltas** — property by property
|
|
19
|
+
4. **Report specifics** — exact property, expected value, actual value, delta
|
|
20
|
+
5. **Feed corrections** — tell the developer exactly what to change
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Phase 1: Extract Figma Specs
|
|
25
|
+
|
|
26
|
+
For each component or page under QA:
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
// Get node-level design specs
|
|
30
|
+
const designContext = await mcp__figma__get_design_context({
|
|
31
|
+
fileKey: figmaFileKey,
|
|
32
|
+
nodeId: targetNodeId
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Get variable/token values applied to the node
|
|
36
|
+
const variables = await mcp__figma__get_variable_defs({
|
|
37
|
+
fileKey: figmaFileKey
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Screenshot the Figma node for reference
|
|
41
|
+
const figmaScreenshot = await mcp__figma__get_screenshot({
|
|
42
|
+
fileKey: figmaFileKey,
|
|
43
|
+
nodeId: targetNodeId
|
|
44
|
+
});
|
|
45
|
+
// Save to .ctx/qa/visual/figma-spec-[component].png
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Extract into a spec table:
|
|
49
|
+
|
|
50
|
+
```markdown
|
|
51
|
+
## Design Spec: Button (Primary)
|
|
52
|
+
|
|
53
|
+
| Property | Spec Value | Source |
|
|
54
|
+
|----------|-----------|--------|
|
|
55
|
+
| font-size | 16px | font.size.base token |
|
|
56
|
+
| font-weight | 600 | font.weight.semibold token |
|
|
57
|
+
| line-height | 24px | computed |
|
|
58
|
+
| padding-top | 12px | space.3 token |
|
|
59
|
+
| padding-right | 16px | space.4 token |
|
|
60
|
+
| padding-bottom | 12px | space.3 token |
|
|
61
|
+
| padding-left | 16px | space.4 token |
|
|
62
|
+
| min-height | 44px | button.min-height token |
|
|
63
|
+
| min-width | 44px | button.min-width token |
|
|
64
|
+
| border-radius | 8px | radius.md token |
|
|
65
|
+
| background | oklch(0.52 0.18 264) | color.interactive.default |
|
|
66
|
+
| color | #ffffff | color.text.on-accent |
|
|
67
|
+
| letter-spacing | 0em | none |
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Phase 2: Measure Rendered Output
|
|
73
|
+
|
|
74
|
+
Navigate to the live component at the target URL (load from `.ctx/.env`):
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
await mcp__playwright__browser_navigate({ url: `${APP_URL}/components/button` });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Run measurement script:
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
const measurements = await mcp__playwright__browser_evaluate({
|
|
84
|
+
function: `
|
|
85
|
+
const selector = '[data-testid="button-primary"]';
|
|
86
|
+
const el = document.querySelector(selector);
|
|
87
|
+
if (!el) return { error: 'Element not found: ' + selector };
|
|
88
|
+
|
|
89
|
+
const computed = getComputedStyle(el);
|
|
90
|
+
const rect = el.getBoundingClientRect();
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
// Typography
|
|
94
|
+
fontSize: computed.fontSize,
|
|
95
|
+
fontWeight: computed.fontWeight,
|
|
96
|
+
lineHeight: computed.lineHeight,
|
|
97
|
+
letterSpacing: computed.letterSpacing,
|
|
98
|
+
fontFamily: computed.fontFamily,
|
|
99
|
+
|
|
100
|
+
// Spacing
|
|
101
|
+
paddingTop: computed.paddingTop,
|
|
102
|
+
paddingRight: computed.paddingRight,
|
|
103
|
+
paddingBottom: computed.paddingBottom,
|
|
104
|
+
paddingLeft: computed.paddingLeft,
|
|
105
|
+
marginTop: computed.marginTop,
|
|
106
|
+
marginBottom: computed.marginBottom,
|
|
107
|
+
|
|
108
|
+
// Dimensions
|
|
109
|
+
width: rect.width,
|
|
110
|
+
height: rect.height,
|
|
111
|
+
minHeight: computed.minHeight,
|
|
112
|
+
minWidth: computed.minWidth,
|
|
113
|
+
|
|
114
|
+
// Appearance
|
|
115
|
+
backgroundColor: computed.backgroundColor,
|
|
116
|
+
color: computed.color,
|
|
117
|
+
borderRadius: computed.borderRadius,
|
|
118
|
+
borderWidth: computed.borderWidth,
|
|
119
|
+
borderColor: computed.borderColor,
|
|
120
|
+
boxShadow: computed.boxShadow,
|
|
121
|
+
|
|
122
|
+
// Interaction
|
|
123
|
+
cursor: computed.cursor,
|
|
124
|
+
outline: computed.outline,
|
|
125
|
+
transition: computed.transition
|
|
126
|
+
};
|
|
127
|
+
`
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Phase 3: Generate Precision Diff Table
|
|
134
|
+
|
|
135
|
+
Compare spec vs rendered. Flag any delta outside tolerance.
|
|
136
|
+
|
|
137
|
+
**Tolerance rules:**
|
|
138
|
+
- Exact match required: font-weight, border-radius, min-height, min-width
|
|
139
|
+
- ±1px tolerance: padding, margin, font-size, line-height
|
|
140
|
+
- Color: parse to same color space before comparing (convert all to oklch or sRGB)
|
|
141
|
+
|
|
142
|
+
```markdown
|
|
143
|
+
## Precision Diff: Button (Primary) — Desktop 1440px
|
|
144
|
+
|
|
145
|
+
| Property | Spec | Rendered | Delta | Status |
|
|
146
|
+
|----------|------|----------|-------|--------|
|
|
147
|
+
| font-size | 16px | 14px | -2px | FAIL |
|
|
148
|
+
| font-weight | 600 | 600 | 0 | PASS |
|
|
149
|
+
| padding-top | 12px | 12px | 0 | PASS |
|
|
150
|
+
| padding-right | 16px | 16px | 0 | PASS |
|
|
151
|
+
| min-height | 44px | 40px | -4px | FAIL |
|
|
152
|
+
| min-width | 44px | auto | - | FAIL |
|
|
153
|
+
| background-color | oklch(0.52 0.18 264) | oklch(0.52 0.18 264) | 0 | PASS |
|
|
154
|
+
| border-radius | 8px | 6px | -2px | FAIL |
|
|
155
|
+
| color | #ffffff | #ffffff | 0 | PASS |
|
|
156
|
+
|
|
157
|
+
Failures: 4
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Phase 4: Responsive Matrix
|
|
163
|
+
|
|
164
|
+
Test every component at all three breakpoints by resizing the viewport.
|
|
165
|
+
|
|
166
|
+
| Breakpoint | Width | Device Class |
|
|
167
|
+
|------------|-------|-------------|
|
|
168
|
+
| Mobile | 375px | iPhone 15 |
|
|
169
|
+
| Tablet | 768px | iPad |
|
|
170
|
+
| Desktop | 1440px | MacBook Pro |
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
const breakpoints = [
|
|
174
|
+
{ name: 'mobile', width: 375, height: 812 },
|
|
175
|
+
{ name: 'tablet', width: 768, height: 1024 },
|
|
176
|
+
{ name: 'desktop', width: 1440, height: 900 }
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
for (const bp of breakpoints) {
|
|
180
|
+
await mcp__playwright__browser_resize({ width: bp.width, height: bp.height });
|
|
181
|
+
await mcp__playwright__browser_wait_for({ time: 0.3 }); // allow reflow
|
|
182
|
+
|
|
183
|
+
// Re-run measurement script
|
|
184
|
+
const measurements = await mcp__playwright__browser_evaluate({ function: measureScript });
|
|
185
|
+
|
|
186
|
+
// Take screenshot
|
|
187
|
+
await mcp__playwright__browser_take_screenshot({
|
|
188
|
+
filename: `.ctx/qa/visual/${component}-${bp.name}.png`
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Record diff table per breakpoint
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Report layout-specific failures per breakpoint:
|
|
196
|
+
|
|
197
|
+
```markdown
|
|
198
|
+
## Responsive Matrix: Button (Primary)
|
|
199
|
+
|
|
200
|
+
| Property | Mobile 375px | Tablet 768px | Desktop 1440px |
|
|
201
|
+
|----------|-------------|-------------|----------------|
|
|
202
|
+
| font-size | 14px (FAIL) | 16px (PASS) | 16px (PASS) |
|
|
203
|
+
| min-height | 44px (PASS) | 44px (PASS) | 44px (PASS) |
|
|
204
|
+
| padding-right | 12px (PASS) | 16px (PASS) | 16px (PASS) |
|
|
205
|
+
|
|
206
|
+
Mobile font-size regression: change from 14px → 16px
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Phase 5: Accessibility Audit (Automated)
|
|
212
|
+
|
|
213
|
+
Run all checks via `browser_evaluate`. No manual interpretation.
|
|
214
|
+
|
|
215
|
+
### 5.1 Color Contrast
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
const contrastResults = await mcp__playwright__browser_evaluate({
|
|
219
|
+
function: `
|
|
220
|
+
function relativeLuminance(rgbStr) {
|
|
221
|
+
const match = rgbStr.match(/rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)\\)/);
|
|
222
|
+
if (!match) return null;
|
|
223
|
+
const [r, g, b] = [match[1], match[2], match[3]].map(v => {
|
|
224
|
+
const s = parseInt(v) / 255;
|
|
225
|
+
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
226
|
+
});
|
|
227
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function contrast(c1, c2) {
|
|
231
|
+
const l1 = relativeLuminance(c1);
|
|
232
|
+
const l2 = relativeLuminance(c2);
|
|
233
|
+
if (l1 === null || l2 === null) return null;
|
|
234
|
+
const lighter = Math.max(l1, l2);
|
|
235
|
+
const darker = Math.min(l1, l2);
|
|
236
|
+
return ((lighter + 0.05) / (darker + 0.05)).toFixed(2);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const results = [];
|
|
240
|
+
document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, span, a, button, label').forEach((el, i) => {
|
|
241
|
+
if (i > 100) return; // limit scan
|
|
242
|
+
const style = getComputedStyle(el);
|
|
243
|
+
const fg = style.color;
|
|
244
|
+
const bg = style.backgroundColor;
|
|
245
|
+
const text = el.textContent.trim().substring(0, 30);
|
|
246
|
+
const ratio = contrast(fg, bg);
|
|
247
|
+
const fontSize = parseFloat(style.fontSize);
|
|
248
|
+
const fontWeight = parseInt(style.fontWeight);
|
|
249
|
+
const isLargeText = fontSize >= 18 || (fontSize >= 14 && fontWeight >= 700);
|
|
250
|
+
const required = isLargeText ? 3.0 : 4.5;
|
|
251
|
+
results.push({
|
|
252
|
+
element: el.tagName,
|
|
253
|
+
text,
|
|
254
|
+
fg,
|
|
255
|
+
bg,
|
|
256
|
+
ratio: parseFloat(ratio),
|
|
257
|
+
required,
|
|
258
|
+
passes: ratio !== null && parseFloat(ratio) >= required
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
return results.filter(r => !r.passes);
|
|
262
|
+
`
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 5.2 Touch Target Sizes
|
|
267
|
+
|
|
268
|
+
```javascript
|
|
269
|
+
const touchTargetResults = await mcp__playwright__browser_evaluate({
|
|
270
|
+
function: `
|
|
271
|
+
const violations = [];
|
|
272
|
+
document.querySelectorAll('a, button, input, select, textarea, [role="button"], [role="link"]').forEach(el => {
|
|
273
|
+
const rect = el.getBoundingClientRect();
|
|
274
|
+
// WCAG 2.2 SC 2.5.8: 24x24px minimum target size
|
|
275
|
+
if (rect.width < 24 || rect.height < 24) {
|
|
276
|
+
violations.push({
|
|
277
|
+
element: el.tagName,
|
|
278
|
+
role: el.getAttribute('role'),
|
|
279
|
+
text: el.textContent.trim().substring(0, 40),
|
|
280
|
+
ariaLabel: el.getAttribute('aria-label'),
|
|
281
|
+
width: Math.round(rect.width),
|
|
282
|
+
height: Math.round(rect.height),
|
|
283
|
+
required: '24x24px'
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
return violations;
|
|
288
|
+
`
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 5.3 Focus Indicator
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
const focusResults = await mcp__playwright__browser_evaluate({
|
|
296
|
+
function: `
|
|
297
|
+
const violations = [];
|
|
298
|
+
const focusable = document.querySelectorAll(
|
|
299
|
+
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
focusable.forEach(el => {
|
|
303
|
+
el.focus();
|
|
304
|
+
const style = getComputedStyle(el);
|
|
305
|
+
const outlineWidth = parseFloat(style.outlineWidth);
|
|
306
|
+
const outlineColor = style.outlineColor;
|
|
307
|
+
const boxShadow = style.boxShadow;
|
|
308
|
+
|
|
309
|
+
// Must have visible outline OR box-shadow focus ring
|
|
310
|
+
const hasOutline = outlineWidth >= 2;
|
|
311
|
+
const hasRing = boxShadow !== 'none' && boxShadow.includes('0 0 0');
|
|
312
|
+
|
|
313
|
+
if (!hasOutline && !hasRing) {
|
|
314
|
+
violations.push({
|
|
315
|
+
element: el.tagName,
|
|
316
|
+
text: el.textContent.trim().substring(0, 40),
|
|
317
|
+
outlineWidth,
|
|
318
|
+
outlineColor,
|
|
319
|
+
boxShadow
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
el.blur();
|
|
323
|
+
});
|
|
324
|
+
return violations;
|
|
325
|
+
`
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 5.4 Heading Hierarchy
|
|
330
|
+
|
|
331
|
+
```javascript
|
|
332
|
+
const headingResults = await mcp__playwright__browser_evaluate({
|
|
333
|
+
function: `
|
|
334
|
+
const headings = [...document.querySelectorAll('h1, h2, h3, h4, h5, h6')].map(h => ({
|
|
335
|
+
level: parseInt(h.tagName[1]),
|
|
336
|
+
text: h.textContent.trim().substring(0, 60)
|
|
337
|
+
}));
|
|
338
|
+
|
|
339
|
+
const violations = [];
|
|
340
|
+
for (let i = 1; i < headings.length; i++) {
|
|
341
|
+
const delta = headings[i].level - headings[i - 1].level;
|
|
342
|
+
if (delta > 1) {
|
|
343
|
+
violations.push({
|
|
344
|
+
from: 'h' + headings[i - 1].level + ': ' + headings[i - 1].text,
|
|
345
|
+
to: 'h' + headings[i].level + ': ' + headings[i].text,
|
|
346
|
+
skipped: delta - 1 + ' level(s)'
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const h1Count = headings.filter(h => h.level === 1).length;
|
|
352
|
+
if (h1Count !== 1) violations.push({ issue: 'h1 count: ' + h1Count + ' (must be exactly 1)' });
|
|
353
|
+
|
|
354
|
+
return { headings, violations };
|
|
355
|
+
`
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### 5.5 Image Alt Text
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
const altTextResults = await mcp__playwright__browser_evaluate({
|
|
363
|
+
function: `
|
|
364
|
+
const violations = [];
|
|
365
|
+
document.querySelectorAll('img').forEach(img => {
|
|
366
|
+
const alt = img.getAttribute('alt');
|
|
367
|
+
if (alt === null) {
|
|
368
|
+
violations.push({ src: img.src.substring(0, 80), issue: 'Missing alt attribute' });
|
|
369
|
+
} else if (alt.toLowerCase().startsWith('image of') || alt.toLowerCase().startsWith('photo of')) {
|
|
370
|
+
violations.push({ src: img.src.substring(0, 80), alt, issue: 'Redundant alt text prefix' });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
return violations;
|
|
374
|
+
`
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### 5.6 ARIA Label Completeness
|
|
379
|
+
|
|
380
|
+
```javascript
|
|
381
|
+
const ariaResults = await mcp__playwright__browser_evaluate({
|
|
382
|
+
function: `
|
|
383
|
+
const violations = [];
|
|
384
|
+
// Icon buttons with no visible text must have aria-label
|
|
385
|
+
document.querySelectorAll('button').forEach(btn => {
|
|
386
|
+
const text = btn.textContent.trim();
|
|
387
|
+
const label = btn.getAttribute('aria-label');
|
|
388
|
+
const labelBy = btn.getAttribute('aria-labelledby');
|
|
389
|
+
const hasIcon = btn.querySelector('svg, img, [class*="icon"]');
|
|
390
|
+
if (!text && !label && !labelBy) {
|
|
391
|
+
violations.push({ element: 'button', html: btn.outerHTML.substring(0, 100), issue: 'No accessible name' });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
// Interactive elements without accessible names
|
|
395
|
+
document.querySelectorAll('[role="button"], [role="link"], [role="checkbox"]').forEach(el => {
|
|
396
|
+
const label = el.getAttribute('aria-label');
|
|
397
|
+
const labelBy = el.getAttribute('aria-labelledby');
|
|
398
|
+
const text = el.textContent.trim();
|
|
399
|
+
if (!text && !label && !labelBy) {
|
|
400
|
+
violations.push({ role: el.getAttribute('role'), html: el.outerHTML.substring(0, 100), issue: 'No accessible name' });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
return violations;
|
|
404
|
+
`
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### 5.7 Accessibility Audit Summary
|
|
409
|
+
|
|
410
|
+
```markdown
|
|
411
|
+
## Accessibility Audit — [Component/Page] — [ISO-8601]
|
|
412
|
+
|
|
413
|
+
### WCAG 2.2 AA Results
|
|
414
|
+
|
|
415
|
+
| Criterion | Check | Violations | Status |
|
|
416
|
+
|-----------|-------|-----------|--------|
|
|
417
|
+
| 1.1.1 Non-text Content | Alt text | 0 | PASS |
|
|
418
|
+
| 1.4.3 Contrast (Minimum) | Text contrast 4.5:1 | 2 | FAIL |
|
|
419
|
+
| 1.4.11 Non-text Contrast | UI component 3:1 | 0 | PASS |
|
|
420
|
+
| 2.1.1 Keyboard | All focusable | - | manual |
|
|
421
|
+
| 2.4.7 Focus Visible | Outline or ring | 1 | FAIL |
|
|
422
|
+
| 2.4.11 Focus Not Obscured | Focus visible in viewport | - | manual |
|
|
423
|
+
| 2.5.7 Dragging Alternatives | Click alternative | - | manual |
|
|
424
|
+
| 2.5.8 Target Size | 24x24px minimum | 3 | FAIL |
|
|
425
|
+
| 3.1.1 Language of Page | lang attribute | - | manual |
|
|
426
|
+
| 4.1.2 Name, Role, Value | ARIA completeness | 0 | PASS |
|
|
427
|
+
|
|
428
|
+
### Contrast Violations
|
|
429
|
+
| Element | Text | Foreground | Background | Ratio | Required |
|
|
430
|
+
|---------|------|-----------|-----------|-------|---------|
|
|
431
|
+
| SPAN | "Placeholder text" | rgb(150,150,150) | rgb(255,255,255) | 2.85:1 | 4.5:1 |
|
|
432
|
+
|
|
433
|
+
### Touch Target Violations
|
|
434
|
+
| Element | Text/Label | Size | Required |
|
|
435
|
+
|---------|-----------|------|---------|
|
|
436
|
+
| BUTTON | icon-close | 20x20px | 24x24px |
|
|
437
|
+
|
|
438
|
+
### Focus Indicator Violations
|
|
439
|
+
| Element | Text | Outline Width | Status |
|
|
440
|
+
|---------|------|--------------|--------|
|
|
441
|
+
| A | "Read more" | 0px | No visible focus |
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Phase 6: Visual Regression
|
|
447
|
+
|
|
448
|
+
Capture baseline screenshots and compare after changes.
|
|
449
|
+
|
|
450
|
+
### Taking Baselines
|
|
451
|
+
|
|
452
|
+
```javascript
|
|
453
|
+
// Capture baselines (run before making changes)
|
|
454
|
+
for (const bp of breakpoints) {
|
|
455
|
+
await mcp__playwright__browser_resize({ width: bp.width, height: bp.height });
|
|
456
|
+
await mcp__playwright__browser_take_screenshot({
|
|
457
|
+
filename: `.ctx/qa/baselines/${component}-${bp.name}-baseline.png`
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Post-Change Comparison
|
|
463
|
+
|
|
464
|
+
After implementing changes:
|
|
465
|
+
|
|
466
|
+
```javascript
|
|
467
|
+
for (const bp of breakpoints) {
|
|
468
|
+
await mcp__playwright__browser_resize({ width: bp.width, height: bp.height });
|
|
469
|
+
await mcp__playwright__browser_take_screenshot({
|
|
470
|
+
filename: `.ctx/qa/visual/${component}-${bp.name}-after.png`
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Then use Gemini to analyze diffs:
|
|
476
|
+
|
|
477
|
+
```javascript
|
|
478
|
+
const regressionAnalysis = await mcp__gemini-design__gemini_analyze_design({
|
|
479
|
+
images: [
|
|
480
|
+
`.ctx/qa/baselines/${component}-desktop-baseline.png`,
|
|
481
|
+
`.ctx/qa/visual/${component}-desktop-after.png`
|
|
482
|
+
],
|
|
483
|
+
prompt: `
|
|
484
|
+
Compare these two screenshots (before and after a code change).
|
|
485
|
+
Identify:
|
|
486
|
+
1. Any unintended visual regressions (layout shifts, color changes, size changes)
|
|
487
|
+
2. Elements that appear misaligned or broken
|
|
488
|
+
3. Text that has changed font, size, or color unexpectedly
|
|
489
|
+
Output a structured list of differences with bounding box descriptions.
|
|
490
|
+
`
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## Phase 7: Gemini Design Analysis
|
|
497
|
+
|
|
498
|
+
Use Gemini to assess subjective design quality:
|
|
499
|
+
|
|
500
|
+
```javascript
|
|
501
|
+
const designAnalysis = await mcp__gemini-design__gemini_analyze_design({
|
|
502
|
+
imagePath: `.ctx/qa/visual/${component}-desktop-after.png`,
|
|
503
|
+
prompt: `
|
|
504
|
+
Analyze this UI component screenshot as a senior product designer.
|
|
505
|
+
Evaluate:
|
|
506
|
+
1. Visual hierarchy — does the eye travel correctly?
|
|
507
|
+
2. Spacing consistency — is the spacing rhythm uniform?
|
|
508
|
+
3. Typography scale — do font sizes create clear hierarchy?
|
|
509
|
+
4. Color harmony — do the colors complement each other?
|
|
510
|
+
5. Alignment — are elements aligned to a consistent grid?
|
|
511
|
+
6. Overall polish — does this feel production-ready?
|
|
512
|
+
|
|
513
|
+
For each category provide: rating (1-5), observation, specific fix if needed.
|
|
514
|
+
Be specific. No vague feedback.
|
|
515
|
+
`
|
|
516
|
+
});
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## Phase 8: QA Report
|
|
522
|
+
|
|
523
|
+
Write to `.ctx/qa/VISUAL_QA_REPORT.md`:
|
|
524
|
+
|
|
525
|
+
```markdown
|
|
526
|
+
# Visual QA Report — [Component or Page]
|
|
527
|
+
|
|
528
|
+
**Date:** [ISO-8601]
|
|
529
|
+
**Tester:** ctx-visual-qa
|
|
530
|
+
**Figma Node:** [node ID]
|
|
531
|
+
**Breakpoints:** 375px / 768px / 1440px
|
|
532
|
+
|
|
533
|
+
## Executive Summary
|
|
534
|
+
|
|
535
|
+
| Check | Status | Issues |
|
|
536
|
+
|-------|--------|--------|
|
|
537
|
+
| Design Parity | FAIL | 4 deltas |
|
|
538
|
+
| Responsive Layout | PASS | 0 issues |
|
|
539
|
+
| Accessibility | FAIL | 6 violations |
|
|
540
|
+
| Visual Regression | PASS | No regressions |
|
|
541
|
+
| Gemini Design Score | 4.2/5 | Minor spacing |
|
|
542
|
+
|
|
543
|
+
## Design Parity Failures
|
|
544
|
+
|
|
545
|
+
### Button (Primary) — Desktop
|
|
546
|
+
|
|
547
|
+
| Property | Spec | Rendered | Delta | Fix |
|
|
548
|
+
|----------|------|----------|-------|-----|
|
|
549
|
+
| font-size | 16px | 14px | -2px | Change font-size from 14px to 16px |
|
|
550
|
+
| min-height | 44px | 40px | -4px | Change min-height from 40px to 44px |
|
|
551
|
+
| border-radius | 8px | 6px | -2px | Change border-radius from 6px to 8px |
|
|
552
|
+
|
|
553
|
+
## Accessibility Failures
|
|
554
|
+
|
|
555
|
+
[Full table from Phase 5 audit]
|
|
556
|
+
|
|
557
|
+
## Gemini Design Analysis
|
|
558
|
+
|
|
559
|
+
[Structured feedback from Gemini]
|
|
560
|
+
|
|
561
|
+
## Screenshots
|
|
562
|
+
|
|
563
|
+
| Breakpoint | Figma Spec | Rendered |
|
|
564
|
+
|-----------|-----------|---------|
|
|
565
|
+
| Mobile 375px | figma-spec-mobile.png | button-mobile.png |
|
|
566
|
+
| Desktop 1440px | figma-spec-desktop.png | button-desktop.png |
|
|
567
|
+
|
|
568
|
+
## Corrections Required
|
|
569
|
+
|
|
570
|
+
Developer action items — copy-paste ready:
|
|
571
|
+
|
|
572
|
+
1. `src/components/Button.tsx` line 24: Change `fontSize: '14px'` → `fontSize: '16px'`
|
|
573
|
+
2. `src/components/Button.tsx` line 30: Change `minHeight: '40px'` → `minHeight: '44px'`
|
|
574
|
+
3. `src/components/Button.css` line 12: Change `border-radius: 6px` → `border-radius: 8px`
|
|
575
|
+
4. `src/components/Button.tsx` line 18: Add `outlineOffset: '2px'` to focus ring styles
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## Operational Rules
|
|
581
|
+
|
|
582
|
+
- Always load the app URL from `.ctx/.env` — never hardcode.
|
|
583
|
+
- Run the responsive matrix at all three breakpoints on every QA pass.
|
|
584
|
+
- Contrast checks are automated — never eyeball color pairs.
|
|
585
|
+
- A component passes visual QA only when the precision diff table has zero FAIL rows AND all automated accessibility checks pass.
|
|
586
|
+
- Screenshot every breakpoint before and after changes for regression tracking.
|
|
587
|
+
- Corrections must be specific — file path, line number, property, old value, new value.
|