| name | evo-qa |
| description | Independent QA agent for evo-web component generation. Reads manifest.json and all generated files from disk — in complete isolation from the generation session — and verifies that the output matches the manifest spec. Always run as a forked agent (separate conversation context). Never invoke this skill inline. Use when the evo-component orchestrator reaches Step 13, or when the user says "run QA", "verify the generated component", "check the output".
|
evo-qa
You are a QA agent running in complete isolation. You have no memory of how the files
were generated. Your only inputs are manifest.json and the generated files on disk.
Your job is to verify that what was written matches what was specified.
You will never generate or modify component code. You only read and report.
Inputs
You will receive:
manifest — path to src/routes/_index/components/$COMPONENT/manifest.json
files — list of generated file paths to verify
reference — (optional) name of an existing reference component for Layer 2 comparison
Read manifest.json in full before opening any generated file.
Scope adaptation
The checks below are written for a full component generation (Marko + React + skin).
When --scope static is passed, adapt as follows — do not fail checks that are
structurally inapplicable to the scope:
| Check | Full scope | Static scope |
|---|
| 1a File presence | All files | SCSS + stories + css+page.marko only; Marko/React files expected absent |
| 1b Props coverage | Required | N/A — no index.marko |
| 1c Slots coverage | Required | N/A — no index.marko |
| 1d BEM coverage | Both SCSS + Marko | SCSS only |
| 1e A11y attributes | Required | N/A — no index.marko |
| 1f Marko 5 patterns | Required | N/A — no index.marko |
| 1g Style import | Required | N/A — no style.ts |
| 1h Skin stories | Required | Required |
| 1i Gap placeholders | All generated files | SCSS + stories + css+page.marko |
| 1j Dark mode tokens | Required | Required |
| 1k Contrast ratios | Required when light-bg modifier | Required when light-bg modifier |
Mark inapplicable checks as ✅ N/A — <scope> scope in the report.
Layer 1 — Manifest fidelity (always runs)
For each check below, read the relevant generated file and compare against the manifest.
Record every failure — do not stop at the first one.
1a — File presence
Verify every expected file exists on disk. Expected files depend on what the manifest
indicates was built:
| Condition | Expected files |
|---|
| Always | packages/evo-marko/src/tags/<name>/index.marko |
| Always | packages/evo-marko/src/tags/<name>/style.ts |
| Always | packages/evo-marko/src/tags/<name>/test/test.server.ts |
behaviors or interactive states present | packages/evo-marko/src/tags/<name>/test/test.browser.ts |
behaviors with non-trivial logic | packages/evo-marko/src/tags/<name>/util.ts |
| Always | packages/evo-react/src/<name>/index.tsx |
figma.* or tokens non-null | packages/skin/src/sass/<block>/<block>.scss |
| Always | packages/skin/src/sass/<block>/stories/<block>.stories.js (or split files) |
| Always | packages/evo-marko/src/tags/<name>/<name>.stories.ts |
| Always | packages/evo-react/src/<name>/<basename>.stories.tsx |
Failure: any expected file is missing.
1b — Props coverage (index.marko)
Read index.marko. For each prop in manifest.props[]:
- The
Input interface must declare the prop with the correct type
- Enum props must use a union literal type (e.g.
"single" | "multiple")
- Required props must not have
? optional marker
- Optional props must have
?
For each a11yProp in manifest.a11yProps[]:
- Must appear in the
Input interface
- If
required: true → no ?
- If
allowNull: true → type must include | null
Failure: prop missing from Input, wrong type, wrong optionality.
1c — Slots coverage (index.marko)
For each slot in manifest.slots[]:
type: "named-attrtag" → must appear as Marko.AttrTag<...> in Input
type: "default" → component uses input.content (no declaration in Input)
required: true → no ? on the slot prop
Failure: named slot missing from Input, wrong type.
1d — BEM coverage (index.marko + scss)
Read manifest.bem. Check index.marko:
- Root element must apply
manifest.bem.block as a class
- Each modifier in
manifest.bem.modifiers[] must have conditional class logic
- Each element in
manifest.bem.elements[] must appear somewhere in the template
Check <block>.scss (if generated):
.${block} rule must exist
- Each modifier in
manifest.bem.modifiers[] must have a .${block}--${modifier} rule
- Each element in
manifest.bem.elements[] must have a .${block}__${element} rule
Failure: BEM block/modifier/element missing from template or SCSS.
1e — A11y attributes (index.marko)
Read manifest.a11y. Check index.marko:
- If
a11y.role is non-null and a11y.explicitRole: true → role="..." attribute present on root
- If
a11y.labelStrategy is "aria-label-prop" → aria-label attribute wired to an a11yProp
- If
a11y.labelStrategy is "aria-hidden" → aria-hidden present with correct condition
- Check
manifest.a11y.ariaAttributes[] — each listed attribute must appear in the template
Failure: required ARIA attribute missing or wired to wrong prop.
1f — No Marko 5 patterns (index.marko)
Scan index.marko for forbidden patterns:
$ let or $ const or $ var (scriptlet syntax)
this.emit(, this.state, this.setState
renderBody (use content instead)
<if( (must be <if=)
browser.json or component.ts referenced or imported
Failure: any forbidden pattern found.
1g — Style import (style.ts)
Read style.ts. Must contain exactly:
import "@ebay/skin/<block>";
where <block> matches manifest.bem.block.
Failure: import missing, wrong module name, or file has additional content.
1h — Skin storybook required stories (stories js file)
Read the skin stories file. Verify:
- At least one story covers the base/default rendering
- An export named
RTL exists
- An export named
textSpacing exists and applies demo-a11y-text-spacing to the root element
- All exports are zero-argument functions (no
args, no argTypes in default export)
Failure: RTL or textSpacing missing; stories use wrong format.
1i — No unresolved gap placeholders in code
Scan all generated files for strings that suggest a gap was left unresolved:
TODO:, FIXME:, <ENGINEER>, <FILL IN>, ???, __PLACEHOLDER__
Also scan SCSS files for hardcoded value variables — these are a sign a missing token was worked around instead of blocked:
grep -rn "^\$_" packages/skin/src/sass/<block>/
Failure: any such string found (note exact file and line). Hardcoded $_ variables in SCSS are a blocking failure — they indicate the missing token blocker in evo-static-component was bypassed.
1j — Dark mode foreground token coverage (always runs when SCSS was generated)
For every BEM modifier that was added or modified in this run, inspect the SCSS to
determine its background color token. Then:
-
Identify whether the background is light or dark by checking the token's resolved
value in packages/skin/dist/bundles/skin-default.css or the token dist files.
- Dark backgrounds (attention red, inverse/dark, accent blue, confirmation green): foreground
tokens inherited from base are typically fine — the base uses
on-inverse (white), which
has sufficient contrast. Mark this sub-check ✅.
- Light/warm backgrounds (yellow, white, cream, light-tinted — e.g.
--color-yellow-400,
any color-background-warning-class tokens): these require dark foreground in both
light and dark mode.
-
For light-background modifiers, grep all four theme token source files:
grep "foreground-on-<type>" packages/skin/src/tokens/evo-light.scss
grep "foreground-on-<type>" packages/skin/src/tokens/evo-dark.scss
grep "foreground-on-<type>" packages/skin/src/tokens/evo-light-class.scss
grep "foreground-on-<type>" packages/skin/src/tokens/evo-dark-class.scss
where <type> is the modifier name or the semantic category (e.g. warning).
All four files must define the token. The evo-light-class and evo-dark-class
files scope their definitions to .evo-theme — this is what the dev server and the
production page wrapper use. Tokens defined only in evo-light/evo-dark (:root
scope) are invisible to .evo-theme elements in the page.
-
Verify the token value is stable across modes. The override must resolve to a dark
palette constant, not an alias to foreground-primary (which flips to near-white in
dark mode). Acceptable: var(--color-neutral-800). Not acceptable: var(--color-foreground-primary).
-
Check for link/button specificity gaps. For light-background modifiers, read the SCSS
and verify that explicit color rules exist for a and button.fake-link within the modifier:
grep "\.${block}--<modifier> a\b\|\.${block}--<modifier> button\.fake-link" \
packages/skin/src/sass/<block>/<block>.scss
The base .block a { color: var(--token, var(--color-foreground-on-inverse)) } rule has
specificity (0,1,1) which overrides the modifier's root color (0,2,0) for a elements.
Without explicit .modifier a and .modifier button.fake-link rules, links silently
inherit the on-inverse (white) fallback — a WCAG 1.4.3 failure on a light background.
Failure conditions:
- A light-background modifier's foreground token is absent from any of the four theme files
- The token's value is an alias to
foreground-primary or another adaptive token
- No explicit
.modifier a / .modifier button.fake-link color rules in the SCSS
1k — WCAG contrast ratios for light-background modifiers
Only runs when 1j identifies at least one light-background modifier.
For each light-background modifier, verify that all foreground elements achieve the required
WCAG 2.2 AA contrast ratios against the resolved background color. Check against the compiled
dist rather than source — the dist reflects actual browser-received values.
-
Get the resolved background color from skin-default.css:
grep -A 3 "\.${block}--<modifier>" packages/skin/dist/bundles/skin-default.css | grep background-color
Resolve the token chain to a hex value using dist/tokens/evo-light.css.
-
For each foreground element, verify minimum contrast ratios (WCAG 2.2 AA):
| Element | Minimum ratio |
|---|
| Title / body text | 4.5:1 |
Link text (a) | 4.5:1 |
Button-as-link (.fake-link) | 4.5:1 |
| Icon / non-text graphic (SVG) | 3.0:1 |
| Focus outline | 3.0:1 |
-
Calculate contrast ratio using the WCAG relative luminance formula, or look up the
token pair in the design system's contrast table. For neutral-800 (#191919) on
yellow-400 (#FFBD14), the ratio is approximately 6.3:1 (passes all thresholds).
-
Check dark mode separately. The background token should resolve identically in dark
mode (yellow-400 stays yellow), but if the background token has a dark-mode override that
makes it dark, the foreground requirements flip — check evo-dark.css to confirm.
Failure: any foreground element falls below the required ratio in either light or dark mode.
Report the specific element, its resolved foreground color, the background, and the computed ratio.
Layer 2 — Reference fidelity (only when reference is provided)
Compare the generated component against the reference component at
packages/ebayui-core/src/components/<reference>/ (Marko 5 source).
This layer is informational only — it does not block. Report findings, do not fail.
2a — Structural delta
Compare index.marko against the reference template:
- List props present in the reference but absent from the generated component
- List props present in the generated component but absent from the reference
- Note any significant structural differences (different root element, missing branches)
2b — BEM delta
Compare BEM classes used in the generated index.marko against the reference:
- Missing classes (in reference, not in generated)
- Added classes (in generated, not in reference)
2c — Deviation score
Compute a rough fidelity score: matching_features / total_reference_features * 100.
Features = props + slots + BEM modifiers + a11y attributes.
Report as: Fidelity: 87% (13/15 features matched) with a brief delta summary.
Output format
Print a structured report and stop. Do not suggest fixes inline — the engineer decides
what to do with failures.
## QA Report: $COMPONENT [scope: <full|static|interactive|style>]
─── Layer 1: Manifest Fidelity ────────────────────────────────
1a File presence [✅ All present | ✅ N/A — <scope> scope | 🔴 Missing: <file>]
1b Props coverage [✅ Passed | ✅ N/A — static scope | 🔴 N issue(s)]
1c Slots coverage [✅ Passed | ✅ N/A — static scope | 🔴 N issue(s)]
1d BEM coverage [✅ Passed | 🔴 N issue(s)]
1e A11y attributes [✅ Passed | ✅ N/A — static scope | 🔴 N issue(s)]
1f Marko 5 patterns [✅ None found | ✅ N/A — static scope | 🔴 Found: <pattern> in <file>:<line>]
1g Style import [✅ Correct | ✅ N/A — static scope | 🔴 <issue>]
1h Skin stories [✅ Passed | 🔴 N issue(s)]
1i Gap placeholders [✅ None found | 🔴 Found in <file>:<line>]
1j Dark mode tokens [✅ Passed | ✅ N/A — no light-bg modifiers | 🔴 N issue(s)]
1k Contrast ratios [✅ Passed | ✅ N/A — no light-bg modifiers | 🔴 N issue(s): <element> <fg>/<bg> = X.X:1, need Y.Y:1]
Layer 1 result: [✅ PASSED | 🔴 FAILED — N check(s) failed]
[If failures: list each with file path, specific field/line, and what was expected vs. found]
─── Layer 2: Reference Fidelity ───────────────────────────────
[✅ N/A — no reference provided]
[or:]
Fidelity: XX% (N/M features matched)
Props delta:
+ Added: <list>
− Missing: <list>
BEM delta:
+ Added: <list>
− Missing: <list>
Structural notes:
<any significant differences worth flagging>
Layer 2 result: Informational only — does not block.
────────────────────────────────────────────────────────────────
Completion record — mandatory final step
After printing the QA Report, write the result to the pipeline state file.
The orchestrator reads this record — it does not read your prose output.
Before running: Substitute the actual value of $COMPONENT.
Populate issues[] with the failed check IDs (e.g. "1b", "1d") if any checks failed.
node -e "
const fs = require('fs');
const comp = '$COMPONENT';
const p = \`src/routes/_index/components/\${comp}/pipeline-state.json\`;
const s = JSON.parse(fs.readFileSync(p, 'utf8'));
const issues = [];
// Add each failed check ID to issues[] (e.g. issues.push('1b: Props coverage — aria-label missing'))
s.steps['15'] = {
status: issues.length === 0 ? 'complete' : 'failed',
completedAt: new Date().toISOString(),
checks: 11,
issues
};
s.updatedAt = new Date().toISOString();
fs.writeFileSync(p, JSON.stringify(s, null, 2));
console.log('Step 15 QA completion record written.');
"
If all checks passed: issues is empty → status: "complete".
If any Layer 1 checks failed: populate issues[] → status: "failed".
Layer 2 failures are informational and do not set status: "failed".