@williambeto/ai-workflow 2.3.7 → 2.4.1

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,56 @@
1
+ ## [2.4.1] - 2026-06-21
2
+
3
+ ### Fixed
4
+ - Hardened visual and accessibility quality gates.
5
+ - Enhanced `ArtifactFidelityGate` to check all skill folder path variations for `frontend-design-system`.
6
+ - Documented Playwright, browser, and Axe-core preconditions in README and quickstart documentation.
7
+ - Improved validation resiliency by warning instead of crashing when browser binaries are missing.
8
+
9
+ ## [2.4.0] - 2026-06-20
10
+
11
+ ### Added
12
+ - Native Visual Evidence capture in `collect-evidence` via `--visual-dist` and `--port` parameters, launching a static loopback server and capturing Chromium screenshots using Playwright.
13
+ - 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.
14
+ - 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`.
15
+ - 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.
16
+ - Automated tests covering static loopback servers, Playwright screenshots, Axe-core checks, inline style gates, CSS tokens, and HTML semantics.
17
+
18
+ ## [2.3.7] - 2026-06-20
19
+
20
+ ### Changed
21
+ - Bumped version to 2.3.7.
22
+
23
+ ## [2.3.6] - 2026-06-20
24
+
25
+ ### Added
26
+ - Request-aware delivery decision and artifact fidelity gates (GREEN phase) to prevent false-positive completions in frontend/backend stack tasks.
27
+ - Hardened check for blocking delivery decisions in `ArtifactFidelityGate` and `Finalizer`.
28
+ - False-positive regression tests and release readiness audit documentation.
29
+
30
+ ### Fixed
31
+ - Aligned WordPress root detection logic between `DeliveryDecisionEngine` and `ArtifactFidelityGate`.
32
+ - Corrected architecture and design patterns policy relative paths in the technical plan prompt.
33
+ - Normalized approved release decision string to `RELEASE_OR_DEPLOY_APPROVED`.
34
+
35
+ ## [2.3.5] - 2026-06-20
36
+
37
+ ### Fixed
38
+ - Corrected the governance policy relative path header in all standard skill Markdown files.
39
+
40
+ ## [2.3.4] - 2026-06-20
41
+
42
+ ### Fixed
43
+ - Corrected the mapping and resolution path of policies under `opencode/docs`.
44
+
45
+ ## [2.3.3] - 2026-06-20
46
+
47
+ ### Added
48
+ - First phase implementation of the Delivery Decision Engine and Artifact Fidelity Gate checking for correct technology stacks.
49
+ - Hardened PromptContracts and validation scripts for stack-aware quality checking.
50
+
1
51
  ## [2.3.2] - 2026-06-19
2
52
 
53
+
3
54
  ### Documentation
4
55
  - Clarified first-time usage with `npx @williambeto/ai-workflow init`.
5
56
  - Clarified local installed-package alias usage with `npx aw init`, `npx aw doctor`, and `npx aw run`.
package/README.md CHANGED
@@ -47,6 +47,14 @@ The `aw` binary is an alias for the same CLI entrypoint as `ai-workflow`.
47
47
 
48
48
  The CLI receives the natural request, classifies the intent, plans the workflow, switch/creates branch safely, delegates coding tasks to the OpenCode runtime adapter, validates changes, and runs bounded remediation if necessary before outputting the handoff summary.
49
49
 
50
+ ### Visual & Accessibility (A11y) Requirements
51
+
52
+ For projects utilizing visual validation (screenshots) or accessibility audits (Axe-core), ensure you have:
53
+ - `playwright` (or `@playwright/test`) and `axe-core` installed under `devDependencies`.
54
+ - Browser binaries installed on the host machine by running `npx playwright install` (specifically Chromium).
55
+
56
+ If these dependencies or browser binaries are missing, the visual and accessibility validations will fail gracefully with warning logs or block depending on CLI validation options.
57
+
50
58
  ## Modes and profiles
51
59
 
52
60
  | Mode | Use for | Primary evidence |
@@ -21,3 +21,9 @@ Write-capable work never runs on `main` or `master`. Existing relevant tests/bui
21
21
  - `BLOCKED`
22
22
 
23
23
  Workflow documents are optional unless they provide lasting value or full mode requires them.
24
+
25
+ ## Visual & Accessibility Evidence Requirements
26
+
27
+ If your project utilizes visual validation (screenshots) or accessibility audits (Axe-core), ensure you have:
28
+ - `playwright` (or `@playwright/test`) and `axe-core` installed under `devDependencies`.
29
+ - Browser binaries installed on the host machine by running `npx playwright install`.
@@ -6,6 +6,7 @@ Required skills:
6
6
 
7
7
  - `frontend-development`
8
8
  - `ui-ux-design`
9
+ - `frontend-design-system`
9
10
 
10
11
  Objective checks:
11
12
 
@@ -6,6 +6,7 @@ Required skills:
6
6
 
7
7
  - `frontend-development`
8
8
  - `ui-ux-design`
9
+ - `frontend-design-system`
9
10
 
10
11
  Objective checks:
11
12
 
@@ -0,0 +1,99 @@
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. The first viewport must read as a single unified composition, not a cluttered dashboard (unless specifically requested).
30
+ - **Brand First**: On branded pages, the brand or product name must be a hero-level signal, not just nav text or an eyebrow. No headline should overpower the brand.
31
+ - **Brand Test**: If the first viewport could belong to another brand after removing the navigation/logo, the branding is too weak.
32
+ - **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. Use expressive, purposeful fonts and avoid generic default stacks (e.g., plain system, Inter, Roboto, Arial) unless matching an existing design system.
33
+ - **Background Atmosphere**: Don't rely on flat, single-color backgrounds. Use smooth gradients, images, or subtle patterns to build a premium atmosphere.
34
+ - **Full-bleed Hero Only**: On landing pages and promotional surfaces, the hero image should be a dominant edge-to-edge visual plane or background by default. Do not use inset hero images, side-panel hero images, rounded media cards, tiled collages, or floating image blocks unless the existing design system clearly requires it.
35
+ - **Hero Budget**: The first viewport must contain only the brand, one headline, one short supporting sentence, one CTA group, and one dominant image. Do not place stats, schedules, event listings, address blocks, promos, "this week" callouts, metadata rows, or secondary marketing content in the first viewport.
36
+ - **No Hero Overlays**: Do not place detached labels, floating badges, promo stickers, info chips, or callout boxes on top of hero media.
37
+ - **No Card Abuse (Default No Cards)**: Never use cards in the hero. Cards are allowed only when they serve as a container for a user interaction. If removing a border, shadow, background, or radius does not hurt interaction or understanding, it should not be a card.
38
+ - **One Job Per Section**: Each page section must have one clear purpose, one headline, and one short supporting sentence.
39
+ - **Real Visual Anchor**: Imagery must show the product, place, atmosphere, or context. Decorative gradients and abstract backgrounds do not count as the main visual idea.
40
+ - **Reduce Clutter**: Avoid pill clusters, stat strips, icon rows, boxed promos, schedule snippets, and multiple competing text blocks.
41
+ - **Motion/Animations**: Use motion to create presence and hierarchy, not noise. Implement at least 2-3 intentional, subtle motions or transitions for visually led work.
42
+ - **React Patterns (when using React)**: Prefer modern React patterns including `useEffectEvent`, `startTransition`, and `useDeferredValue` when appropriate. Do not add `useMemo`/`useCallback` by default unless already used; follow the repository's React Compiler guidance.
43
+ - **Strict Design System / CSS Gate**:
44
+ - CSS tokens must define a primary palette, background, surface, text styles, and spacing scale.
45
+ - Exposing excessive inline styles (`style="..."`) is prohibited.
46
+ - CDN dependencies for visual libraries (Tailwind, Bootstrap) are blocked unless explicitly authorized.
47
+ - **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.
48
+ - **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`.
49
+ - **Regras Anti-Output Fraco**: Prevent empty blocks, broken states, and plain gradient fills with no concrete content.
50
+ - **Exception**: If working within an existing website or design system, preserve the established patterns, structure, and visual language.
51
+
52
+ ## Allowed tools
53
+ - `view_file` to inspect existing design tokens, components, and templates.
54
+ - `write_to_file` and `replace_file_content` to edit CSS variables and structured HTML markup.
55
+ - `run_command` to execute validate command line checks (`npx aw validate --a11y` and `collect-evidence`).
56
+
57
+ ## Forbidden actions
58
+ - Deploying raw HTML without CSS custom properties/tokens.
59
+ - Injecting raw CDN CSS/JS libraries into page headers without authorization.
60
+ - Committing unstyled layout states or missing loading/empty/error/success conditions.
61
+ - Placing cards, stats, event listings, address blocks, metadata rows, or secondary marketing clutter in the first viewport.
62
+ - Placing floating badges, chips, or labels on top of hero media.
63
+ - Using inset, side-panel, rounded media cards, or tiled collages for hero images.
64
+ - Using generic browser-default font stacks (Inter, Roboto, Arial, system) for branded layouts.
65
+
66
+ ## Procedure
67
+ 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-*).
68
+ 2. **Draft Semantic Structure**: Build the UI skeleton using HTML5 semantic elements (`<header>`, `<main>`, etc.) with unique IDs for testing.
69
+ 3. **Apply Spacing, Typography, and Motion Scales**: Implement the 8px grid system, a tight heading line-height / relaxed body line-height scale, expressive font selections, and at least 2-3 intentional motion animations.
70
+ 4. **Implement UI States**: Ensure loading, empty, error, and success states are styled, fully functional, and visually robust.
71
+ 5. **A11y Verification**: Verify WCAG contrast levels. Run validation commands with `npm run validate` or `npx aw validate --a11y` to check for accessibility violations.
72
+ 6. **Collect Visual Evidence**: Capture desktop and mobile screenshots using `collect-evidence --visual-dist` to verify design fidelity.
73
+
74
+ ## Expected output schema
75
+ The output must report:
76
+ - CSS variables/tokens detected or introduced
77
+ - Accessibility audit summary (Axe-core run results)
78
+ - UI states implemented (loading, empty, error, success)
79
+ - Semantic HTML tags list
80
+ - Visual evidence location (`.evidence/visual/`)
81
+ - Compliance decision: PASS | FAIL (with detailed reasons)
82
+
83
+ ## Evidence required
84
+ - Screenshot artifacts in `.evidence/visual/` (mobile and desktop)
85
+ - Axe-core accessibility report in `.evidence/a11y/`
86
+ - Verified HTML/CSS files conforming to the design gates
87
+
88
+ ## Stop conditions
89
+ - Absence of CSS variables/tokens defining colors or spacing
90
+ - Axe-core report containing critical a11y violations
91
+ - Exceeding inline style limits or using unauthorized CDNs
92
+
93
+ ## Failure modes
94
+ - **White screenshots or rendering failures**: Local server path or assets not loaded.
95
+ - **Unstyled outputs**: Styles not applied or missing CSS files.
96
+ - **Broken layout states**: Misaligned responsive viewports or broken container grids.
97
+
98
+ ## Escalation rules
99
+ - Report critical design system deviations to the frontend lead or escalate to the primary owner.
@@ -15,3 +15,9 @@
15
15
  Quick and standard work return a concise summary with branch, changed areas, observed validation, and limitations. Full/release/audit/security work may persist structured evidence.
16
16
 
17
17
  Use `ai-workflow collect-evidence --mode=full` when a durable validation artefact is required. Do not use workflow files as substitutes for actual commands.
18
+
19
+ ## Visual & Accessibility Evidence Requirements
20
+
21
+ If your project utilizes visual validation (screenshots) or accessibility audits (Axe-core), ensure you have:
22
+ - `playwright` (or `@playwright/test`) and `axe-core` installed under `devDependencies`.
23
+ - Browser binaries installed on the host machine by running `npx playwright install`.
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.1",
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,93 @@ export class ArtifactFidelityGate {
315
328
  violations
316
329
  };
317
330
  }
331
+
332
+ async verifyFrontendFidelity(changedFiles) {
333
+ const skillPaths = [
334
+ path.join(this.cwd, ".agents/skills/frontend-design-system/SKILL.md"),
335
+ path.join(this.cwd, ".agents/skills/frontend_design_system/SKILL.md"),
336
+ path.join(this.cwd, ".ai-workflow/opencode/skills/frontend-design-system/SKILL.md"),
337
+ path.join(this.cwd, ".ai-workflow/opencode/skills/frontend_design_system/SKILL.md"),
338
+ path.join(this.cwd, "opencode/skills/frontend-design-system/SKILL.md"),
339
+ path.join(this.cwd, "opencode/skills/frontend_design_system/SKILL.md")
340
+ ];
341
+
342
+ let hasDesignSystemSkill = false;
343
+ for (const p of skillPaths) {
344
+ const exists = await fs.access(p).then(() => true).catch(() => false);
345
+ if (exists) {
346
+ hasDesignSystemSkill = true;
347
+ break;
348
+ }
349
+ }
350
+
351
+ if (!hasDesignSystemSkill) {
352
+ return null;
353
+ }
354
+
355
+ const uiFiles = changedFiles.filter((file) =>
356
+ /\.(tsx|jsx|vue|html|css|scss|less|blade\.php)$/.test(file)
357
+ );
358
+ if (uiFiles.length === 0) return null;
359
+
360
+ const violations = [];
361
+ let cssContentCombined = "";
362
+ let htmlContentCombined = "";
363
+ let styleAttrsCount = 0;
364
+
365
+ for (const file of uiFiles) {
366
+ try {
367
+ const fullPath = path.join(this.cwd, file);
368
+ const content = await fs.readFile(fullPath, "utf8");
369
+
370
+ if (/\.(css|scss|less|vue|tsx|jsx)$/.test(file)) {
371
+ cssContentCombined += content;
372
+ }
373
+ if (/\.(html|vue|tsx|jsx|blade\.php)$/.test(file)) {
374
+ htmlContentCombined += content;
375
+
376
+ // Match inline style attributes
377
+ const inlineStyles = content.match(/style\s*=\s*["']/g) || [];
378
+ styleAttrsCount += inlineStyles.length;
379
+
380
+ // CDN Checks
381
+ if (/(unpkg\.com|cdnjs\.cloudflare\.com|cdn\.jsdelivr\.net|tailwindcss|bootstrap)/i.test(content)) {
382
+ const isApproved = (this.explicitApprovals || []).some(app =>
383
+ /cdn-styles|tailwind|bootstrap/i.test(app)
384
+ );
385
+ if (!isApproved) {
386
+ violations.push(`Fidelity Violation: standalone external CDN style/library dependency detected in '${file}' without explicit authorization.`);
387
+ }
388
+ }
389
+ }
390
+ } catch {
391
+ // ignore
392
+ }
393
+ }
394
+
395
+ // 1. Tokens CSS mínimos Check
396
+ const hasRules = cssContentCombined.includes("{");
397
+ const hasCssVars = /--[a-zA-Z0-9_-]+\s*:/g.test(cssContentCombined);
398
+ if (hasRules && !hasCssVars) {
399
+ violations.push("Fidelity Violation: Design System token mismatch. No CSS custom properties (variables) detected in changed UI stylesheets.");
400
+ }
401
+
402
+ // 2. Excesso de style="..."
403
+ if (styleAttrsCount > 5) {
404
+ violations.push(`Fidelity Violation: excessive inline styles detected (${styleAttrsCount} style="..." declarations). Use CSS classes or utility classes instead.`);
405
+ }
406
+
407
+ // 3. HTML principal tags semânticas básicas Check
408
+ if (htmlContentCombined.length > 0) {
409
+ const hasSemanticTags = /<(header|main|footer|nav|section)\b/i.test(htmlContentCombined);
410
+ const hasH1 = /<h1\b/i.test(htmlContentCombined);
411
+ if (!hasSemanticTags && !hasH1) {
412
+ violations.push("Fidelity Violation: HTML layout structure lacks basic semantic elements (<header>, <main>, <footer>, <nav>, <section>) or <h1> tag.");
413
+ }
414
+ }
415
+
416
+ return violations;
417
+ }
318
418
  }
319
419
 
320
420
  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,175 @@
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
+ let browser;
69
+ try {
70
+ browser = await playwright.chromium.launch();
71
+ } catch (err) {
72
+ await this.closeServer();
73
+ throw new Error(
74
+ `Failed to launch Playwright Chromium. Please ensure browsers are installed using 'npx playwright install chromium'. Details: ${err.message}`
75
+ );
76
+ }
77
+ const page = await browser.newPage();
78
+
79
+ const outputDir = path.join(this.cwd, ".evidence/visual");
80
+ await fs.mkdir(outputDir, { recursive: true });
81
+
82
+ const desktopPath = path.join(outputDir, "desktop.png");
83
+ const mobilePath = path.join(outputDir, "mobile.png");
84
+
85
+ try {
86
+ await page.goto(`http://127.0.0.1:${this.port}`);
87
+
88
+ // Desktop
89
+ await page.setViewportSize({ width: 1440, height: 900 });
90
+ await page.screenshot({ path: desktopPath, fullPage: true });
91
+
92
+ // Mobile
93
+ await page.setViewportSize({ width: 390, height: 844 });
94
+ await page.screenshot({ path: mobilePath, fullPage: true });
95
+ } finally {
96
+ await browser.close();
97
+ await this.closeServer();
98
+ }
99
+
100
+ return [desktopPath, mobilePath];
101
+ }
102
+
103
+ async runAccessibilityCheck() {
104
+ if (!this.visualDist) {
105
+ throw new Error("Accessibility check requires a target build directory specified via --visual-dist.");
106
+ }
107
+
108
+ let playwright;
109
+ try {
110
+ playwright = await import("playwright");
111
+ } catch {
112
+ throw new Error(
113
+ "Playwright is required to run accessibility checks. Please install it using 'npm install -D playwright' and initialize browsers using 'npx playwright install'."
114
+ );
115
+ }
116
+
117
+ await this.startServer();
118
+ let browser;
119
+ try {
120
+ browser = await playwright.chromium.launch();
121
+ } catch (err) {
122
+ await this.closeServer();
123
+ throw new Error(
124
+ `Failed to launch Playwright Chromium. Please ensure browsers are installed using 'npx playwright install chromium'. Details: ${err.message}`
125
+ );
126
+ }
127
+ const page = await browser.newPage();
128
+
129
+ const outputDir = path.join(this.cwd, ".evidence/a11y");
130
+ await fs.mkdir(outputDir, { recursive: true });
131
+
132
+ try {
133
+ await page.goto(`http://127.0.0.1:${this.port}`);
134
+
135
+ // Read axe-core bundle
136
+ let axeScript = "";
137
+ try {
138
+ const axePath = path.join(this.cwd, "node_modules/axe-core/axe.min.js");
139
+ axeScript = await fs.readFile(axePath, "utf8");
140
+ } catch {
141
+ // Fallback: look in dev-kit node_modules
142
+ try {
143
+ // Resolve relative to current module path
144
+ const mainAxePath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../node_modules/axe-core/axe.min.js");
145
+ axeScript = await fs.readFile(mainAxePath, "utf8");
146
+ } catch {
147
+ throw new Error("axe-core package is not installed. Please install axe-core using 'npm install -D axe-core'.");
148
+ }
149
+ }
150
+
151
+ await page.evaluate(axeScript);
152
+
153
+ const axeResults = await page.evaluate(() => {
154
+ return window.axe.run();
155
+ });
156
+
157
+ const reportPath = path.join(outputDir, "report.json");
158
+ await fs.writeFile(reportPath, JSON.stringify(axeResults, null, 2));
159
+
160
+ const criticalViolations = axeResults.violations.filter(v => v.impact === "critical");
161
+ const seriousViolations = axeResults.violations.filter(v => v.impact === "serious");
162
+
163
+ return {
164
+ passed: criticalViolations.length === 0,
165
+ reportPath,
166
+ violations: axeResults.violations,
167
+ critical: criticalViolations,
168
+ serious: seriousViolations
169
+ };
170
+ } finally {
171
+ await browser.close();
172
+ await this.closeServer();
173
+ }
174
+ }
175
+ }
@@ -1,14 +1,14 @@
1
1
  const PROFILE_DEFINITIONS = Object.freeze({
2
2
  "frontend-product": Object.freeze({
3
3
  owner: "Astra",
4
- skills: ["frontend-development", "ui-ux-design"],
4
+ skills: ["frontend-development", "ui-ux-design", "frontend-design-system"],
5
5
  objective: "Deliver a user-facing product or marketing surface with truthful copy and deliberate visual composition.",
6
6
  requiredChecks: ["tests", "typecheck", "build", "responsive-render", "primary-interaction", "truthfulness"],
7
7
  forbiddenAssumptions: ["fixed SaaS section sequence", "mandatory pricing", "mandatory testimonials", "subjective premium score"]
8
8
  }),
9
9
  "frontend-utility": Object.freeze({
10
10
  owner: "Astra",
11
- skills: ["frontend-development", "ui-ux-design"],
11
+ skills: ["frontend-development", "ui-ux-design", "frontend-design-system"],
12
12
  objective: "Deliver a focused user tool with a complete primary flow and clear operational states.",
13
13
  requiredChecks: ["tests", "typecheck", "build", "responsive-render", "primary-interaction"],
14
14
  forbiddenAssumptions: ["marketing sections", "pricing", "testimonials", "commercial narrative"]