@williambeto/ai-workflow 2.3.7 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
+ ## [2.4.0] - 2026-06-20
2
+
3
+ ### Added
4
+ - Native Visual Evidence capture in `collect-evidence` via `--visual-dist` and `--port` parameters, launching a static loopback server and capturing Chromium screenshots using Playwright.
5
+ - CSS/HTML Design System Gate validation in `ArtifactFidelityGate` that automatically runs when the `frontend-design-system` skill is loaded in the repository, checking for CSS custom properties, inline style density, CDN scripts, and semantic HTML layout tags.
6
+ - Native accessibility auditing command `npx aw validate --a11y` via Axe-core running inside Playwright, blocking critical violations and persisting reports in `.evidence/a11y/report.json`.
7
+ - Standard `frontend-design-system` skill (`.agents/skills/frontend-design-system/SKILL.md`) to guide Astra and other agents through visual hierarchy, spacing grids, theme palettes, accessible contrast, and output rules.
8
+ - Automated tests covering static loopback servers, Playwright screenshots, Axe-core checks, inline style gates, CSS tokens, and HTML semantics.
9
+
10
+ ## [2.3.7] - 2026-06-20
11
+
12
+ ### Changed
13
+ - Bumped version to 2.3.7.
14
+
15
+ ## [2.3.6] - 2026-06-20
16
+
17
+ ### Added
18
+ - Request-aware delivery decision and artifact fidelity gates (GREEN phase) to prevent false-positive completions in frontend/backend stack tasks.
19
+ - Hardened check for blocking delivery decisions in `ArtifactFidelityGate` and `Finalizer`.
20
+ - False-positive regression tests and release readiness audit documentation.
21
+
22
+ ### Fixed
23
+ - Aligned WordPress root detection logic between `DeliveryDecisionEngine` and `ArtifactFidelityGate`.
24
+ - Corrected architecture and design patterns policy relative paths in the technical plan prompt.
25
+ - Normalized approved release decision string to `RELEASE_OR_DEPLOY_APPROVED`.
26
+
27
+ ## [2.3.5] - 2026-06-20
28
+
29
+ ### Fixed
30
+ - Corrected the governance policy relative path header in all standard skill Markdown files.
31
+
32
+ ## [2.3.4] - 2026-06-20
33
+
34
+ ### Fixed
35
+ - Corrected the mapping and resolution path of policies under `opencode/docs`.
36
+
37
+ ## [2.3.3] - 2026-06-20
38
+
39
+ ### Added
40
+ - First phase implementation of the Delivery Decision Engine and Artifact Fidelity Gate checking for correct technology stacks.
41
+ - Hardened PromptContracts and validation scripts for stack-aware quality checking.
42
+
1
43
  ## [2.3.2] - 2026-06-19
2
44
 
45
+
3
46
  ### Documentation
4
47
  - Clarified first-time usage with `npx @williambeto/ai-workflow init`.
5
48
  - Clarified local installed-package alias usage with `npx aw init`, `npx aw doctor`, and `npx aw run`.
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: frontend-design-system
3
+ description: Design system guidelines and rules for creating premium, consistent, and accessible interfaces.
4
+ governance: ../../docs/policies/SKILLS_COMMON_GOVERNANCE.md
5
+ ---
6
+
7
+ # Frontend Design System PromptContract
8
+
9
+ ## Role
10
+ Guides Astra and other agents in styling UI, hierarchy, and responsive layout states to build premium interfaces.
11
+
12
+ ## Objective
13
+ Evolve technical governance to guarantee real visual and frontend quality by enforcing visual thesis, design tokens, responsive states, semantic HTML, and accessibility validations.
14
+
15
+ ## Trusted context
16
+ - Project design system tokens (CSS custom properties)
17
+ - HTML semantic markup standards
18
+ - Accessible color ratios (WCAG 2.1 AA/AAA)
19
+ - Visual asset registries (images, local SVGs)
20
+ - Client styling guidelines
21
+
22
+ ## User input
23
+ - Feature requirements and wireframes
24
+ - Active UI components and styles
25
+ - Target viewport dimensions (mobile, desktop)
26
+ - Feedback on visual quality or layout defects
27
+
28
+ ## Constraints
29
+ - **Visual Thesis & Deliberate Composition**: Layouts must be cohesive. Avoid generic grids, pill soup, or unorganized structures.
30
+ - **Copy Specificity**: Never use placeholders ("Lorem Ipsum"). Write domain-specific, realistic copy.
31
+ - **Strict Design System / CSS Gate**:
32
+ - CSS tokens must define a primary palette, background, surface, text styles, and spacing scale.
33
+ - Exposing excessive inline styles (`style="..."`) is prohibited.
34
+ - CDN dependencies for visual libraries (Tailwind, Bootstrap) are blocked unless explicitly authorized.
35
+ - **Typography Scale**: Maintain a single `<h1>` per page. Line heights must be balanced: 1.1-1.25 for headings, 1.5-1.7 for readable body text.
36
+ - **Acessibilidade (A11y)**: Must achieve WCAG compliance. Contrast ratio must be at least 4.5:1 for body text (3:1 for headings/large text). Semantic HTML elements (`<header>`, `<main>`, `<footer>`, `<nav>`, `<section>`) must structure the layout.
37
+ - **Tokens de Cor & Spacing**: Spacing must adhere to an 8px-grid scale (e.g. 4px, 8px, 16px, 24px, 32px, 48px). Colors must use CSS custom properties: `--color-primary`, `--color-background`, `--color-surface`, `--color-text`.
38
+ - **Regras Anti-Output Fraco**: Prevent empty blocks, broken states, and plain gradient fills with no concrete content.
39
+
40
+ ## Allowed tools
41
+ - `view_file` to inspect existing design tokens, components, and templates.
42
+ - `write_to_file` and `replace_file_content` to edit CSS variables and structured HTML markup.
43
+ - `run_command` to execute validate command line checks (`npx aw validate --a11y` and `collect-evidence`).
44
+
45
+ ## Forbidden actions
46
+ - Deploying raw HTML without CSS custom properties/tokens.
47
+ - Injecting raw CDN CSS/JS libraries into page headers without authorization.
48
+ - Committing unstyled layout states or missing loading/empty/error/success conditions.
49
+
50
+ ## Procedure
51
+ 1. **Analyze existing Design Tokens**: Check if the project already has CSS variables or custom properties. Adapt to the project's tokens if found, otherwise define a default safe set (primary, background, surface, text, space-*, font-*).
52
+ 2. **Draft Semantic Structure**: Build the UI skeleton using HTML5 semantic elements (`<header>`, `<main>`, etc.) with unique IDs for testing.
53
+ 3. **Apply Spacing and Typography Scales**: Implement the 8px grid system and a tight heading line-height / relaxed body line-height scale.
54
+ 4. **Implement UI States**: Ensure loading, empty, error, and success states are styled, fully functional, and visually robust.
55
+ 5. **A11y Verification**: Verify WCAG contrast levels. Run validation commands with `npm run validate` or `npx aw validate --a11y` to check for accessibility violations.
56
+ 6. **Collect Visual Evidence**: Capture desktop and mobile screenshots using `collect-evidence --visual-dist` to verify design fidelity.
57
+
58
+ ## Expected output schema
59
+ The output must report:
60
+ - CSS variables/tokens detected or introduced
61
+ - Accessibility audit summary (Axe-core run results)
62
+ - UI states implemented (loading, empty, error, success)
63
+ - Semantic HTML tags list
64
+ - Visual evidence location (`.evidence/visual/`)
65
+ - Compliance decision: PASS | FAIL (with detailed reasons)
66
+
67
+ ## Evidence required
68
+ - Screenshot artifacts in `.evidence/visual/` (mobile and desktop)
69
+ - Axe-core accessibility report in `.evidence/a11y/`
70
+ - Verified HTML/CSS files conforming to the design gates
71
+
72
+ ## Stop conditions
73
+ - Absence of CSS variables/tokens defining colors or spacing
74
+ - Axe-core report containing critical a11y violations
75
+ - Exceeding inline style limits or using unauthorized CDNs
76
+
77
+ ## Failure modes
78
+ - **White screenshots or rendering failures**: Local server path or assets not loaded.
79
+ - **Unstyled outputs**: Styles not applied or missing CSS files.
80
+ - **Broken layout states**: Misaligned responsive viewports or broken container grids.
81
+
82
+ ## Escalation rules
83
+ - Report critical design system deviations to the frontend lead or escalate to the primary owner.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williambeto/ai-workflow",
3
- "version": "2.3.7",
3
+ "version": "2.4.0",
4
4
  "description": "AI Workflow Kit — OpenCode-first software delivery workflow with agents, commands, skills, validation, and evidence",
5
5
  "license": "MIT",
6
6
  "author": "José Willams",
@@ -92,6 +92,7 @@
92
92
  "@semantic-release/npm": "^13.1.5",
93
93
  "ajv": "^8.20.0",
94
94
  "ajv-formats": "^3.0.1",
95
+ "axe-core": "^4.10.0",
95
96
  "markdownlint-cli": "^0.48.0",
96
97
  "playwright": "^1.60.0",
97
98
  "semantic-release": "^25.0.3",
package/src/cli.js CHANGED
@@ -12,7 +12,8 @@ Usage:
12
12
  ai-workflow execute "<request>" [--task=<slug>] [--request="<request>"]
13
13
  ai-workflow run --spec-path=<path>
14
14
  ai-workflow init [--yes] [--force] [--dry-run] [--no-install] [--no-overwrite] [--gemini] [--claude] [--codex] [--antigravity] [--profile=<profile>]
15
- ai-workflow collect-evidence [--task=<slug>] [--mode=<quick|standard|full>] [--dry-run]
15
+ ai-workflow validate [--a11y] [--visual-dist=<path>] [--port=<port>]
16
+ ai-workflow collect-evidence [--task=<slug>] [--mode=<quick|standard|full>] [--dry-run] [--visual-dist=<path>] [--port=<port>]
16
17
  ai-workflow doctor
17
18
 
18
19
  Commands:
@@ -30,6 +31,8 @@ function parseFlags(args) {
30
31
  const taskArg = args.find((arg) => arg.startsWith("--task="));
31
32
  const modeArg = args.find((arg) => arg.startsWith("--mode="));
32
33
  const requestArg = args.find((arg) => arg.startsWith("--request="));
34
+ const visualDistArg = args.find((arg) => arg.startsWith("--visual-dist="));
35
+ const portArg = args.find((arg) => arg.startsWith("--port="));
33
36
  const profileIdx = args.indexOf("--profile");
34
37
  const profileVal = profileEqArg
35
38
  ? profileEqArg.replace("--profile=", "")
@@ -52,7 +55,10 @@ function parseFlags(args) {
52
55
  profile: profileVal || undefined,
53
56
  taskSlug: taskArg ? taskArg.replace("--task=", "") : undefined,
54
57
  mode: modeArg ? modeArg.replace("--mode=", "") : undefined,
55
- request: requestArg ? requestArg.replace("--request=", "") : undefined
58
+ request: requestArg ? requestArg.replace("--request=", "") : undefined,
59
+ visualDist: visualDistArg ? visualDistArg.replace("--visual-dist=", "") : undefined,
60
+ port: portArg ? portArg.replace("--port=", "") : undefined,
61
+ a11y: args.includes("--a11y")
56
62
  };
57
63
  }
58
64
 
@@ -104,7 +110,27 @@ export async function runCli(args) {
104
110
  if (command === "collect-evidence") {
105
111
  const flags = parseFlags(args.slice(1));
106
112
  if (flags.mode && !["quick", "standard", "full"].includes(flags.mode)) throw new Error("--mode must be quick, standard, or full");
107
- await runCollectEvidence({ cwd: process.cwd(), taskSlug: flags.taskSlug, mode: flags.mode, dryRun: flags.dryRun });
113
+ await runCollectEvidence({
114
+ cwd: process.cwd(),
115
+ taskSlug: flags.taskSlug,
116
+ mode: flags.mode,
117
+ dryRun: flags.dryRun,
118
+ visualDist: flags.visualDist,
119
+ port: flags.port
120
+ });
121
+ return;
122
+ }
123
+
124
+ if (command === "validate") {
125
+ const flags = parseFlags(args.slice(1));
126
+ const { runValidate } = await import("./commands/validate.js");
127
+ await runValidate({
128
+ cwd: process.cwd(),
129
+ taskSlug: flags.taskSlug,
130
+ a11y: flags.a11y,
131
+ visualDist: flags.visualDist,
132
+ port: flags.port
133
+ });
108
134
  return;
109
135
  }
110
136
 
@@ -5,13 +5,13 @@ import { buildDeliverySummary, formatDeliverySummary } from "../core/validation/
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
7
 
8
- export async function runCollectEvidence({ cwd, exitOnError = true, taskSlug = null, mode = null, dryRun = false, profile = "generic", branchRecovery = "NOT_RECORDED", userRequest = null, explicitApprovals = [] }) {
8
+ export async function runCollectEvidence({ cwd, exitOnError = true, taskSlug = null, mode = null, dryRun = false, profile = "generic", branchRecovery = "NOT_RECORDED", userRequest = null, explicitApprovals = [], visualDist = null, port = null }) {
9
9
  const qualityGuard = new QualityGuard({ cwd, taskSlug, mode });
10
10
  const planner = new ValidationPlanner({ cwd, qualityGuard });
11
11
  const tasks = await planner.plan(profile);
12
12
 
13
13
  const persist = !dryRun && mode === "full";
14
- const collector = new EvidenceCollector({ cwd, timeout: 60000, taskSlug, mode, profile, branchRecovery, userRequest, explicitApprovals });
14
+ const collector = new EvidenceCollector({ cwd, timeout: 60000, taskSlug, mode, profile, branchRecovery, userRequest, explicitApprovals, visualDist, port });
15
15
  const evidence = await collector.collect(tasks, { writeArtifact: persist });
16
16
  const summary = buildDeliverySummary({ evidence });
17
17
 
@@ -0,0 +1,81 @@
1
+ import { EvidenceCollector } from "../core/validation/evidence-collector.js";
2
+ import { QualityGuard } from "../core/validation/quality-guard.js";
3
+ import { ValidationPlanner } from "../core/validation/validation-planner.js";
4
+ import { VisualVerifier } from "../core/validation/visual-verifier.js";
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+
8
+ export async function runValidate({ cwd = process.cwd(), exitOnError = true, taskSlug = null, a11y = false, visualDist = null, port = 8080 }) {
9
+ console.log("--- Running Validation ---");
10
+ const qualityGuard = new QualityGuard({ cwd, taskSlug, mode: "standard" });
11
+ const planner = new ValidationPlanner({ cwd, qualityGuard });
12
+ const tasks = await planner.plan("standard");
13
+
14
+ const collector = new EvidenceCollector({ cwd, timeout: 60000, taskSlug, mode: "standard" });
15
+ const results = [];
16
+
17
+ for (const task of tasks) {
18
+ console.log(`Running validation task: ${task.name}...`);
19
+ const res = collector.runTask(task);
20
+ results.push(res);
21
+ console.log(`- Status: ${res.status} (${res.summary})`);
22
+ }
23
+
24
+ let a11yResult = null;
25
+ if (a11y) {
26
+ if (!visualDist) {
27
+ throw new Error("Accessibility audit (--a11y) requires a target build directory specified via --visual-dist=<path>.");
28
+ }
29
+ console.log(`\n--- Running Accessibility Audit ---`);
30
+ console.log(`Serving ${visualDist} on port ${port}...`);
31
+ try {
32
+ const verifier = new VisualVerifier({ cwd, visualDist, port });
33
+ a11yResult = await verifier.runAccessibilityCheck();
34
+ console.log(`Accessibility audit complete.`);
35
+ console.log(`- Critical violations: ${a11yResult.critical.length}`);
36
+ console.log(`- Serious violations: ${a11yResult.serious.length}`);
37
+ console.log(`- Report saved to: ${path.relative(cwd, a11yResult.reportPath)}`);
38
+ } catch (err) {
39
+ console.error(`Accessibility audit failed: ${err.message}`);
40
+ if (exitOnError) process.exit(1);
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ // Load future configuration if present
46
+ let config = {};
47
+ try {
48
+ const configContent = await fs.readFile(path.join(cwd, "ai-workflow.config.json"), "utf8");
49
+ config = JSON.parse(configContent);
50
+ } catch {}
51
+
52
+ const hasFailure = results.some((r) => ["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(r.status));
53
+
54
+ let hasA11yFailure = false;
55
+ if (a11yResult) {
56
+ const criticalIsBlocker = true; // Critical violations are always blockers
57
+ const seriousIsBlocker = config.a11y?.seriousAsBlocker === true;
58
+
59
+ if (criticalIsBlocker && a11yResult.critical.length > 0) {
60
+ hasA11yFailure = true;
61
+ console.error(`Accessibility FAILED: ${a11yResult.critical.length} critical accessibility violations detected.`);
62
+ }
63
+ if (a11yResult.serious.length > 0) {
64
+ if (seriousIsBlocker) {
65
+ hasA11yFailure = true;
66
+ console.error(`Accessibility FAILED: ${a11yResult.serious.length} serious accessibility violations detected (blocked by configuration).`);
67
+ } else {
68
+ console.warn(`Accessibility WARNING: ${a11yResult.serious.length} serious accessibility violations detected.`);
69
+ }
70
+ }
71
+ }
72
+
73
+ if (hasFailure || hasA11yFailure) {
74
+ console.log("\nValidation FAILED.");
75
+ if (exitOnError) process.exit(1);
76
+ return { passed: false, results, a11yResult };
77
+ }
78
+
79
+ console.log("\nValidation PASSED.");
80
+ return { passed: true, results, a11yResult };
81
+ }
@@ -58,6 +58,14 @@ export class ArtifactFidelityGate {
58
58
  }
59
59
  }
60
60
 
61
+ const frontendViolations = await this.verifyFrontendFidelity(changedFiles);
62
+ if (frontendViolations && frontendViolations.length > 0) {
63
+ return {
64
+ passed: false,
65
+ reason: frontendViolations.join("; ")
66
+ };
67
+ }
68
+
61
69
  return { passed: true };
62
70
  }
63
71
 
@@ -305,6 +313,11 @@ export class ArtifactFidelityGate {
305
313
  }
306
314
  }
307
315
 
316
+ const frontendViolations = await this.verifyFrontendFidelity(filteredFiles);
317
+ if (frontendViolations && frontendViolations.length > 0) {
318
+ violations.push(...frontendViolations);
319
+ }
320
+
308
321
  const status = violations.length > 0 ? "BLOCKED" : "PASS";
309
322
  return {
310
323
  status,
@@ -315,6 +328,79 @@ export class ArtifactFidelityGate {
315
328
  violations
316
329
  };
317
330
  }
331
+
332
+ async verifyFrontendFidelity(changedFiles) {
333
+ const dsSkillPath1 = path.join(this.cwd, ".agents/skills/frontend-design-system/SKILL.md");
334
+ const dsSkillPath2 = path.join(this.cwd, ".agents/skills/frontend_design_system/SKILL.md");
335
+ const hasDesignSystemSkill = await fs.access(dsSkillPath1).then(() => true).catch(() => false) ||
336
+ await fs.access(dsSkillPath2).then(() => true).catch(() => false);
337
+ if (!hasDesignSystemSkill) {
338
+ return null;
339
+ }
340
+
341
+ const uiFiles = changedFiles.filter((file) =>
342
+ /\.(tsx|jsx|vue|html|css|scss|less|blade\.php)$/.test(file)
343
+ );
344
+ if (uiFiles.length === 0) return null;
345
+
346
+ const violations = [];
347
+ let cssContentCombined = "";
348
+ let htmlContentCombined = "";
349
+ let styleAttrsCount = 0;
350
+
351
+ for (const file of uiFiles) {
352
+ try {
353
+ const fullPath = path.join(this.cwd, file);
354
+ const content = await fs.readFile(fullPath, "utf8");
355
+
356
+ if (/\.(css|scss|less|vue|tsx|jsx)$/.test(file)) {
357
+ cssContentCombined += content;
358
+ }
359
+ if (/\.(html|vue|tsx|jsx|blade\.php)$/.test(file)) {
360
+ htmlContentCombined += content;
361
+
362
+ // Match inline style attributes
363
+ const inlineStyles = content.match(/style\s*=\s*["']/g) || [];
364
+ styleAttrsCount += inlineStyles.length;
365
+
366
+ // CDN Checks
367
+ if (/(unpkg\.com|cdnjs\.cloudflare\.com|cdn\.jsdelivr\.net|tailwindcss|bootstrap)/i.test(content)) {
368
+ const isApproved = (this.explicitApprovals || []).some(app =>
369
+ /cdn-styles|tailwind|bootstrap/i.test(app)
370
+ );
371
+ if (!isApproved) {
372
+ violations.push(`Fidelity Violation: standalone external CDN style/library dependency detected in '${file}' without explicit authorization.`);
373
+ }
374
+ }
375
+ }
376
+ } catch {
377
+ // ignore
378
+ }
379
+ }
380
+
381
+ // 1. Tokens CSS mínimos Check
382
+ const hasRules = cssContentCombined.includes("{");
383
+ const hasCssVars = /--[a-zA-Z0-9_-]+\s*:/g.test(cssContentCombined);
384
+ if (hasRules && !hasCssVars) {
385
+ violations.push("Fidelity Violation: Design System token mismatch. No CSS custom properties (variables) detected in changed UI stylesheets.");
386
+ }
387
+
388
+ // 2. Excesso de style="..."
389
+ if (styleAttrsCount > 5) {
390
+ violations.push(`Fidelity Violation: excessive inline styles detected (${styleAttrsCount} style="..." declarations). Use CSS classes or utility classes instead.`);
391
+ }
392
+
393
+ // 3. HTML principal tags semânticas básicas Check
394
+ if (htmlContentCombined.length > 0) {
395
+ const hasSemanticTags = /<(header|main|footer|nav|section)\b/i.test(htmlContentCombined);
396
+ const hasH1 = /<h1\b/i.test(htmlContentCombined);
397
+ if (!hasSemanticTags && !hasH1) {
398
+ violations.push("Fidelity Violation: HTML layout structure lacks basic semantic elements (<header>, <main>, <footer>, <nav>, <section>) or <h1> tag.");
399
+ }
400
+ }
401
+
402
+ return violations;
403
+ }
318
404
  }
319
405
 
320
406
  async function hasWpThemeHeader(cwd, file) {
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { QualityGuard } from "./quality-guard.js";
5
5
  import { DeliveryDecisionEngine } from "./delivery-decision-engine.js";
6
6
  import { ArtifactFidelityGate } from "./artifact-fidelity-gate.js";
7
+ import { VisualVerifier } from "./visual-verifier.js";
7
8
 
8
9
  const VALIDATION_KINDS = new Set(["test", "build", "typecheck", "lint", "security", "smoke", "validate", "other"]);
9
10
 
@@ -29,7 +30,7 @@ function inferTaskKind(task = {}) {
29
30
  }
30
31
 
31
32
  export class EvidenceCollector {
32
- constructor({ cwd, maxLogLength = 2000, timeout = 60000, taskSlug = null, mode = null, profile = "generic", branchRecovery = "NOT_RECORDED", userRequest = null, explicitApprovals = [] } = {}) {
33
+ constructor({ cwd, maxLogLength = 2000, timeout = 60000, taskSlug = null, mode = null, profile = "generic", branchRecovery = "NOT_RECORDED", userRequest = null, explicitApprovals = [], visualDist = null, port = 8080 } = {}) {
33
34
  this.cwd = cwd;
34
35
  this.maxLogLength = maxLogLength;
35
36
  this.timeout = timeout;
@@ -39,6 +40,8 @@ export class EvidenceCollector {
39
40
  this.branchRecovery = branchRecovery;
40
41
  this.userRequest = userRequest;
41
42
  this.explicitApprovals = explicitApprovals;
43
+ this.visualDist = visualDist;
44
+ this.port = Number(port || 8080);
42
45
  }
43
46
 
44
47
  async findUserRequest() {
@@ -194,6 +197,26 @@ export class EvidenceCollector {
194
197
  if (check.status === "PASS_WITH_NOTES" && check.reason) limitations.push(`${name}: ${check.reason}`);
195
198
  }
196
199
 
200
+ let visualEvidence = null;
201
+ if (this.visualDist) {
202
+ console.log(`\n--- Capturing Visual Evidence ---`);
203
+ console.log(`Serving ${this.visualDist} on port ${this.port}...`);
204
+ try {
205
+ const verifier = new VisualVerifier({ cwd: this.cwd, visualDist: this.visualDist, port: this.port });
206
+ const screenshots = await verifier.captureScreenshots();
207
+ visualEvidence = {
208
+ desktop: path.relative(this.cwd, screenshots[0]),
209
+ mobile: path.relative(this.cwd, screenshots[1])
210
+ };
211
+ console.log(`Visual evidence captured successfully:`);
212
+ console.log(`- Desktop: ${visualEvidence.desktop}`);
213
+ console.log(`- Mobile: ${visualEvidence.mobile}`);
214
+ } catch (err) {
215
+ console.error(`Error capturing visual evidence: ${err.message}`);
216
+ throw err;
217
+ }
218
+ }
219
+
197
220
  const evidence = {
198
221
  timestamp: new Date().toISOString(),
199
222
  taskSlug: this.taskSlug,
@@ -208,7 +231,8 @@ export class EvidenceCollector {
208
231
  checks: policyValidation.checks,
209
232
  limitations,
210
233
  deliveryDecision,
211
- artifactFidelityCheck
234
+ artifactFidelityCheck,
235
+ visualEvidence
212
236
  };
213
237
 
214
238
  if (writeArtifact) {
@@ -0,0 +1,159 @@
1
+ import http from "node:http";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ export class VisualVerifier {
6
+ constructor({ cwd = process.cwd(), visualDist = null, port = 8080 } = {}) {
7
+ this.cwd = cwd;
8
+ this.visualDist = visualDist;
9
+ this.port = Number(port || 8080);
10
+ this.server = null;
11
+ }
12
+
13
+ async startServer() {
14
+ if (!this.visualDist) {
15
+ throw new Error("No --visual-dist directory specified. Cannot start visual server.");
16
+ }
17
+ const distPath = path.resolve(this.cwd, this.visualDist);
18
+ this.server = http.createServer(async (req, res) => {
19
+ try {
20
+ const urlPath = req.url.split("?")[0];
21
+ const filePath = path.join(distPath, urlPath === "/" ? "index.html" : urlPath);
22
+ const content = await fs.readFile(filePath);
23
+
24
+ const ext = path.extname(filePath).toLowerCase();
25
+ let contentType = "text/html";
26
+ if (ext === ".js" || ext === ".mjs") contentType = "application/javascript";
27
+ else if (ext === ".css") contentType = "text/css";
28
+ else if (ext === ".png") contentType = "image/png";
29
+ else if (ext === ".jpg" || ext === ".jpeg") contentType = "image/jpeg";
30
+ else if (ext === ".svg") contentType = "image/svg+xml";
31
+
32
+ res.writeHead(200, { "Content-Type": contentType });
33
+ res.end(content);
34
+ } catch (err) {
35
+ res.writeHead(404, { "Content-Type": "text/plain" });
36
+ res.end("Not Found");
37
+ }
38
+ });
39
+
40
+ await new Promise((resolve, reject) => {
41
+ this.server.listen(this.port, "127.0.0.1", (err) => {
42
+ if (err) reject(err);
43
+ else resolve();
44
+ });
45
+ });
46
+ }
47
+
48
+ async closeServer() {
49
+ if (this.server) {
50
+ await new Promise((resolve) => this.server.close(resolve));
51
+ this.server = null;
52
+ }
53
+ }
54
+
55
+ async captureScreenshots() {
56
+ if (!this.visualDist) return [];
57
+
58
+ let playwright;
59
+ try {
60
+ playwright = await import("playwright");
61
+ } catch {
62
+ throw new Error(
63
+ "Playwright is required to capture visual evidence. Please install it using 'npm install -D playwright' and initialize browsers using 'npx playwright install'."
64
+ );
65
+ }
66
+
67
+ await this.startServer();
68
+ const browser = await playwright.chromium.launch();
69
+ const page = await browser.newPage();
70
+
71
+ const outputDir = path.join(this.cwd, ".evidence/visual");
72
+ await fs.mkdir(outputDir, { recursive: true });
73
+
74
+ const desktopPath = path.join(outputDir, "desktop.png");
75
+ const mobilePath = path.join(outputDir, "mobile.png");
76
+
77
+ try {
78
+ await page.goto(`http://127.0.0.1:${this.port}`);
79
+
80
+ // Desktop
81
+ await page.setViewportSize({ width: 1440, height: 900 });
82
+ await page.screenshot({ path: desktopPath, fullPage: true });
83
+
84
+ // Mobile
85
+ await page.setViewportSize({ width: 390, height: 844 });
86
+ await page.screenshot({ path: mobilePath, fullPage: true });
87
+ } finally {
88
+ await browser.close();
89
+ await this.closeServer();
90
+ }
91
+
92
+ return [desktopPath, mobilePath];
93
+ }
94
+
95
+ async runAccessibilityCheck() {
96
+ if (!this.visualDist) {
97
+ throw new Error("Accessibility check requires a target build directory specified via --visual-dist.");
98
+ }
99
+
100
+ let playwright;
101
+ try {
102
+ playwright = await import("playwright");
103
+ } catch {
104
+ throw new Error(
105
+ "Playwright is required to run accessibility checks. Please install it using 'npm install -D playwright' and initialize browsers using 'npx playwright install'."
106
+ );
107
+ }
108
+
109
+ await this.startServer();
110
+ const browser = await playwright.chromium.launch();
111
+ const page = await browser.newPage();
112
+
113
+ const outputDir = path.join(this.cwd, ".evidence/a11y");
114
+ await fs.mkdir(outputDir, { recursive: true });
115
+
116
+ try {
117
+ await page.goto(`http://127.0.0.1:${this.port}`);
118
+
119
+ // Read axe-core bundle
120
+ let axeScript = "";
121
+ try {
122
+ const axePath = path.join(this.cwd, "node_modules/axe-core/axe.min.js");
123
+ axeScript = await fs.readFile(axePath, "utf8");
124
+ } catch {
125
+ // Fallback: look in dev-kit node_modules
126
+ try {
127
+ // Resolve relative to current module path
128
+ const mainAxePath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../node_modules/axe-core/axe.min.js");
129
+ axeScript = await fs.readFile(mainAxePath, "utf8");
130
+ } catch {
131
+ throw new Error("axe-core package is not installed. Please install axe-core using 'npm install -D axe-core'.");
132
+ }
133
+ }
134
+
135
+ await page.evaluate(axeScript);
136
+
137
+ const axeResults = await page.evaluate(() => {
138
+ return window.axe.run();
139
+ });
140
+
141
+ const reportPath = path.join(outputDir, "report.json");
142
+ await fs.writeFile(reportPath, JSON.stringify(axeResults, null, 2));
143
+
144
+ const criticalViolations = axeResults.violations.filter(v => v.impact === "critical");
145
+ const seriousViolations = axeResults.violations.filter(v => v.impact === "serious");
146
+
147
+ return {
148
+ passed: criticalViolations.length === 0,
149
+ reportPath,
150
+ violations: axeResults.violations,
151
+ critical: criticalViolations,
152
+ serious: seriousViolations
153
+ };
154
+ } finally {
155
+ await browser.close();
156
+ await this.closeServer();
157
+ }
158
+ }
159
+ }