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.
Files changed (74) hide show
  1. package/README.md +375 -676
  2. package/agents/ctx-arch-mapper.md +5 -3
  3. package/agents/ctx-auditor.md +5 -3
  4. package/agents/ctx-codex-reviewer.md +214 -0
  5. package/agents/ctx-concerns-mapper.md +5 -3
  6. package/agents/ctx-criteria-suggester.md +6 -4
  7. package/agents/ctx-debugger.md +5 -3
  8. package/agents/ctx-designer.md +488 -114
  9. package/agents/ctx-discusser.md +5 -3
  10. package/agents/ctx-executor.md +5 -3
  11. package/agents/ctx-handoff.md +6 -4
  12. package/agents/ctx-learner.md +5 -3
  13. package/agents/ctx-mapper.md +4 -3
  14. package/agents/ctx-ml-analyst.md +600 -0
  15. package/agents/ctx-ml-engineer.md +933 -0
  16. package/agents/ctx-ml-reviewer.md +485 -0
  17. package/agents/ctx-ml-scientist.md +626 -0
  18. package/agents/ctx-parallelizer.md +4 -3
  19. package/agents/ctx-planner.md +5 -3
  20. package/agents/ctx-predictor.md +4 -3
  21. package/agents/ctx-qa.md +5 -3
  22. package/agents/ctx-quality-mapper.md +5 -3
  23. package/agents/ctx-researcher.md +5 -3
  24. package/agents/ctx-reviewer.md +6 -4
  25. package/agents/ctx-team-coordinator.md +5 -3
  26. package/agents/ctx-tech-mapper.md +5 -3
  27. package/agents/ctx-verifier.md +5 -3
  28. package/bin/ctx.js +199 -27
  29. package/commands/brand.md +309 -0
  30. package/commands/ctx.md +10 -10
  31. package/commands/design.md +304 -0
  32. package/commands/experiment.md +251 -0
  33. package/commands/help.md +57 -7
  34. package/commands/init.md +25 -0
  35. package/commands/metrics.md +1 -1
  36. package/commands/milestone.md +1 -1
  37. package/commands/ml-status.md +197 -0
  38. package/commands/monitor.md +1 -1
  39. package/commands/train.md +266 -0
  40. package/commands/visual-qa.md +559 -0
  41. package/commands/voice.md +1 -1
  42. package/hooks/post-tool-use.js +39 -0
  43. package/hooks/pre-tool-use.js +94 -0
  44. package/hooks/subagent-stop.js +32 -0
  45. package/package.json +9 -3
  46. package/plugin.json +46 -0
  47. package/skills/ctx-design-system/SKILL.md +572 -0
  48. package/skills/ctx-ml-experiment/SKILL.md +334 -0
  49. package/skills/ctx-ml-pipeline/SKILL.md +437 -0
  50. package/skills/ctx-orchestrator/SKILL.md +91 -0
  51. package/skills/ctx-review-gate/SKILL.md +147 -0
  52. package/skills/ctx-state/SKILL.md +100 -0
  53. package/skills/ctx-visual-qa/SKILL.md +587 -0
  54. package/src/agents.js +109 -0
  55. package/src/auto.js +287 -0
  56. package/src/capabilities.js +226 -0
  57. package/src/commits.js +94 -0
  58. package/src/config.js +112 -0
  59. package/src/context.js +241 -0
  60. package/src/handoff.js +156 -0
  61. package/src/hooks.js +218 -0
  62. package/src/install.js +125 -50
  63. package/src/lifecycle.js +194 -0
  64. package/src/metrics.js +198 -0
  65. package/src/pipeline.js +269 -0
  66. package/src/review-gate.js +338 -0
  67. package/src/runner.js +120 -0
  68. package/src/skills.js +143 -0
  69. package/src/state.js +267 -0
  70. package/src/worktree.js +244 -0
  71. package/templates/PRD.json +1 -1
  72. package/templates/config.json +4 -237
  73. package/workflows/ctx-router.md +0 -485
  74. 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.