@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 +51 -0
- package/README.md +8 -0
- package/dist-assets/docs/QUICKSTART.md +6 -0
- package/dist-assets/docs/profiles/frontend-product.md +1 -0
- package/dist-assets/docs/profiles/frontend-utility.md +1 -0
- package/dist-assets/skills/frontend-design-system/SKILL.md +99 -0
- package/docs/getting-started/quickstart.md +6 -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 +100 -0
- package/src/core/validation/evidence-collector.js +26 -2
- package/src/core/validation/visual-verifier.js +175 -0
- package/src/core/workflow-profiles.js +2 -2
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`.
|
|
@@ -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
|
+
"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
|
|
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,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"]
|