@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 +43 -0
- package/dist-assets/skills/frontend-design-system/SKILL.md +83 -0
- package/package.json +2 -1
- package/src/cli.js +29 -3
- package/src/commands/collect-evidence.js +2 -2
- package/src/commands/validate.js +81 -0
- package/src/core/validation/artifact-fidelity-gate.js +86 -0
- package/src/core/validation/evidence-collector.js +26 -2
- package/src/core/validation/visual-verifier.js +159 -0
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
|
+
"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
|
|
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({
|
|
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
|
+
}
|