diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..082abd608e --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "node -e \"const fs=require('fs'),path=require('path');const base='src/routes/_index/components';if(!fs.existsSync(base))process.exit(0);const comps=fs.readdirSync(base,{withFileTypes:true}).filter(d=>d.isDirectory()).map(d=>d.name);let found=false;comps.forEach(c=>{const p=path.join(base,c,'pipeline-state.json');if(!fs.existsSync(p))return;try{const s=JSON.parse(fs.readFileSync(p,'utf8'));const ip=Object.entries(s.steps||{}).filter(([,v])=>v.status==='in-progress');if(ip.length){found=true;process.stdout.write('\\n⚠️ PIPELINE IN-PROGRESS — do not stop.\\n Component: '+s.component+'\\n Step '+ip.map(([k])=>k).join(', ')+' is still in-progress.\\n Read pipeline-state.json and advance to the next step before stopping.\\n');}}catch(e){}});if(found)process.exit(2);\" 2>/dev/null" + } + ] + } + ] + } +} diff --git a/.claude/skills/evo-a11y/SKILL.md b/.claude/skills/evo-a11y/SKILL.md new file mode 100644 index 0000000000..6c74665591 --- /dev/null +++ b/.claude/skills/evo-a11y/SKILL.md @@ -0,0 +1,283 @@ +--- +name: evo-a11y +description: > + Accessibility validation and incremental docs generation for evo-web components. + Runs in two passes: Pass 1 (Step 7, after static layer) validates HTML and + static storybook, then writes the static sections of accessibility+page.marko. + Pass 2 (Step 12, after all layers) validates Marko and React, then fills in + the interactive sections. The orchestrator explicitly declares which pass and + scope — never inferred from disk. Skipped entirely for style-only revisions. + Use this whenever the user says "run a11y", "validate accessibility", "generate + accessibility docs", "check ARIA", or when the orchestrator invokes Step 7 + (Pass 1) or Step 12 (Pass 2). +--- + +# evo-a11y + +You run in two passes. The orchestrator tells you which pass and scope — do not +infer from disk state (fails on revisions where old files are already present). + +The accessibility docs are built incrementally: Pass 1 writes the static +sections; Pass 2 fills in the interactive sections. For style-only revisions, +this skill is not invoked. + +--- + +## Scope declaration from orchestrator + +You will receive one of: + +- `"Pass 1 — static layer only. Steps 4–6 have run."` +- `"Pass 2 — full scope. Steps 4–11 have run."` +- `"Pass 2 — interactive scope. Steps 8–11 have run."` + +If invoked standalone without declaration, ask which pass and scope. + +--- + +## Pass 1 — Static layer validation + static a11y docs + +**Trigger:** Step 7, after static component + static storybook + css docs (Steps 4–6) + +### Phase 1a: Validate + +**HTML from `/evo-static-component` (in context):** + +- `manifest.a11y.explicitRole: true` → `role="..."` on root element +- `manifest.a11y.labelStrategy === "aria-label-prop"` → `aria-label` present +- `manifest.a11y.labelStrategy === "aria-hidden"` → `aria-hidden="true"` present for null case +- Each entry in `manifest.a11y.ariaAttributes[]` represented in at least one variant +- Root element matches `manifest.rootElement.default` +- No `
` where a semantic element is correct + +**Static storybook (read from disk):** + +- `RTL` named export exists — 🔴 if missing +- `textSpacing` export with `demo-a11y-text-spacing` on root BEM element — 🔴 if missing + +**Anti-patterns:** + +- `role="presentation"` without justification +- `aria-hidden` and interactive elements mixed incorrectly + +### Phase 1b: Write static sections of accessibility+page.marko + +Write `src/routes/_index/components//accessibility+page.marko` with the +sections derivable from the static layer. Mark interactive sections as stubs. + +```marko +import "../../../../sass/accessibility.scss"; +import { urls } from "../../../../data"; + +
+

Accessibility

+ +

Best Practices

+ + +

Interaction Design

+

This section provides detailed instructions for how different input types + should navigate and operate the pattern.

+ +

Keyboard

+ + +

Screen Reader

+ + +

Pointer

+ + +

ARIA Reference

+ + + + + +
AttributeDescription
+ + +
+``` + +**What to fill vs. stub:** + +| Section | Fill now | Reason | +| ------------------------- | ---------------------------------------- | ------------------------------------------------ | +| Best Practices | ✅ Always | Derivable from manifest.a11y + callerObligations | +| ARIA Reference table | ✅ Always | Derivable from manifest.a11y.ariaAttributes[] | +| Pointer (non-interactive) | ✅ If focusable === false | Fully static | +| Keyboard | ✅ If keyboardInteractions[] in manifest | Otherwise stub | +| Screen Reader | ✅ Static announcements from manifest | Stub interactive state changes | +| Pointer (interactive) | Stub | Needs active/pressed/hover from Marko/React | + +**Non-interactive components:** Fill all sections completely in Pass 1 — there +is no Pass 2 for style/static-only scopes. + +Also write `accessibility+meta.json` if the component is non-interactive (no +Pass 2 needed): + +```json +{ + "pageTitle": " Accessibility Guidelines — ", + "pageDescription": "<2-3 sentence SEO description>" +} +``` + +For interactive components, mark meta as pending: write a stub meta.json and +complete it in Pass 2. + +### Pass 1 output + +``` +## A11y Pass 1 — $COMPONENT + +Validation: + [✅ HTML ARIA correct | 🔴 issue: description] + [✅ RTL story present | 🔴 Missing] + [✅ textSpacing story present | 🔴 Missing] + +Docs written: + ✅ accessibility+page.marko — static sections complete + [✅ all sections filled (non-interactive) | 🔒 interactive stubs pending Pass 2] + +Result: [✅ PASSED — proceed to Marko | 🔴 BLOCKED — fix issues first] +``` + +--- + +## Pass 2 — Full validation + fill interactive a11y docs + +**Trigger:** Step 12, after all layers (Steps 4–11) or interactive-scope layers (Steps 8–11) + +### Phase 2a: Validate all layers + +**Re-run Pass 1 checks** (confirm static is still clean). + +**`index.marko`** — read from disk: + +- `manifest.a11y.labelStrategy` correctly wired: conditional `aria-label`/`aria-hidden` +- a11yProps with `allowNull: true` have both branches +- Required a11yProps: no `?` in `Input` interface +- Each `manifest.a11y.ariaAttributes[]` entry in template +- Keyboard handlers: Marko 6 inline syntax — no `this.emit`, `$ let`, `` for focus state, not class methods + +**`index.tsx`** — read from disk: + +- a11yProps in TypeScript props type +- `aria-*` forwarded to correct DOM element +- Keyboard handlers match `manifest.keyboardInteractions[]` + +**Marko storybook** — if `manifest.keyboardModel`: interactive story with `` or `:=` — 🟡 if missing + +**React storybook** — if `manifest.keyboardModel`: `Controlled` or `render`-based story — 🟡 if missing + +### Phase 2b: Fill interactive sections of accessibility+page.marko + +Read the existing `accessibility+page.marko` (written in Pass 1). Fill in or +replace the stubbed sections: + +**Keyboard section** — from `manifest.keyboardModel` + `manifest.keyboardInteractions[]`: + +- Use `KEY` for key names +- `must` for required behavior; `may` for optional + +**Screen Reader section** — from `manifest.a11y.screenReaderAnnouncement`: + +- Cover each usage mode (decorative, informational, interactive) +- Plain English — what gets announced, not implementation details + +**Pointer section** — from manifest states + behaviors: + +- Active/pressed/clicked behavior +- If inside interactive element: what clicking does + +**Also complete `accessibility+meta.json`** (if written as stub in Pass 1). + +### Pass 2 output + +``` +## A11y Pass 2 — $COMPONENT + +Validation: + Static: [✅ Clean | 🔴 N issue(s)] + Marko: [✅ Passed | 🔴 N issue(s) | 🟡 N warnings] + React: [✅ Passed | 🔴 N issue(s) | 🟡 N warnings] + Marko storybook: [✅ Interactive story present | 🟡 Missing] + React storybook: [✅ Controlled story present | 🟡 Missing] + +Docs updated: + ✅ accessibility+page.marko — interactive sections filled + ✅ accessibility+meta.json — complete + +Result: [✅ PASSED | 🔴 BLOCKED — N issue(s)] +``` + +🔴 blocking issues in Pass 2 must be fixed before the build step. + +--- + +## Completion record — mandatory final step + +After all outputs are verified on disk, write the completion record. +The step ID depends on which pass was declared at invocation. +This is the signal the orchestrator reads to advance — do not skip this step. + +### Pass 1 (Step 7) + +> **Before running:** Substitute the actual component name for `$COMPONENT` and the actual BEM block name for ``. + +```bash +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 block = ''; +s.steps['7'] = { + status: 'complete', + completedAt: new Date().toISOString(), + outputs: [ + \`src/routes/_index/components/\${block}/accessibility+page.marko\`, + ] +}; +s.updatedAt = new Date().toISOString(); +fs.writeFileSync(p, JSON.stringify(s, null, 2)); +console.log('Step 7 completion record written.'); +" +``` + +### Pass 2 (Step 12) + +> **Before running:** Substitute the actual component name for `$COMPONENT` and the actual BEM block name for ``. + +```bash +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 block = ''; +s.steps['12'] = { + status: 'complete', + completedAt: new Date().toISOString(), + outputs: [ + \`src/routes/_index/components/\${block}/accessibility+page.marko\`, + \`src/routes/_index/components/\${block}/accessibility+meta.json\`, + ] +}; +s.updatedAt = new Date().toISOString(); +fs.writeFileSync(p, JSON.stringify(s, null, 2)); +console.log('Step 12 completion record written.'); +" +``` + +If any blocking a11y failure was found that could not be fixed inline, write +`status: "failed"` with the failing check as the `error` field. Use the correct step ID +(`'7'` for Pass 1, `'12'` for Pass 2). diff --git a/.claude/skills/evo-component/SKILL.md b/.claude/skills/evo-component/SKILL.md new file mode 100644 index 0000000000..bb3e1f8914 --- /dev/null +++ b/.claude/skills/evo-component/SKILL.md @@ -0,0 +1,1309 @@ +--- +name: evo-component +description: > + Orchestrator for the evo-web AI component generation pipeline. Reads an + approved manifest.json and generates all component layers in the correct + sequence, with scope-aware step selection. Supports four scopes: full (all + 16 steps, default for new components), static (HTML + SCSS changes), style + (SCSS only), and interactive (framework behavior changes). Run this after + /evo-create-component-manifest and engineer approval. Use this skill whenever + the user says "generate the component", "run evo-component", "start code gen", + "build the component from the manifest", "kick off generation for [component]", + "the manifest is approved, go ahead", "proceed with generation", "looks good, + generate it", "produce the Marko and React files", or any variant of "the + manifest/gap report is approved and I want to generate code now". Do NOT wait + for the user to name this skill explicitly — if they have an approved manifest + and want to generate code, this is the skill to use. +--- + +# Generate Component + +This skill reads an approved `manifest.json` and generates component layers in +a fixed, scope-aware sequence. The manifest is the single source of truth. + +--- + +## Invocation + +``` +/evo-component [--scope ] [--reference ] +``` + +- `component-name` — kebab-case (e.g. `evo-accordion`) +- `--scope` — `full` (default), `static`, `interactive`, or `style` +- `--reference` — triggers Layer 2 fidelity comparison in QA + +If `$ARGUMENTS` is empty, ask: "Which component should I generate?" +Wait. Do not proceed until you have a component name. + +--- + +## Step 1 — Read and validate the manifest + +Read `src/routes/_index/components/$COMPONENT/manifest.json` in full. + +If the file does not exist, stop and report the error. + +--- + +## Step 2 — Gate: resolve blocking gaps + +Scan `manifest.json["gaps"]` for `confidence: "low"` or `source: "missing"`. +If any exist, list them and stop. Medium-confidence gaps proceed as inline +`// TODO: verify` comments. + +--- + +## Step 2.5 — State file management + +The pipeline uses `src/routes/_index/components/$COMPONENT/pipeline-state.json` as a +disk-resident record of step progress. Read it at startup, create it if absent, detect +stalls, and fast-forward past completed steps. + +### Schema + +```json +{ + "component": "accordion", + "scope": "full", + "manifestHash": "", + "startedAt": "2026-06-10T09:00:00.000Z", + "updatedAt": "2026-06-10T09:14:22.000Z", + "steps": { + "4": { "status": "pending" }, + "5": { "status": "pending" }, + "6": { "status": "pending" }, + "6.5": { "status": "pending" }, + "7": { "status": "pending" }, + "micro-qa-1": { "status": "pending" }, + "8": { "status": "pending" }, + "9": { "status": "pending" }, + "10": { "status": "pending" }, + "11": { "status": "pending" }, + "12": { "status": "pending" }, + "micro-qa-2": { "status": "pending" }, + "13": { "status": "pending" }, + "14": { "status": "pending" }, + "15": { "status": "pending" } + } +} +``` + +`status` values: `pending` | `in-progress` | `complete` | `failed` | `skipped` + +### Startup procedure + +> **Note on `$SCOPE`:** This is the value from `--scope` on invocation (default: `full`). The full scope-to-steps mapping is in the scope reference table at the end of this skill. For state file initialization, steps not included in the current scope should be set to `skipped` instead of `pending`. + +**1. Compute the manifest hash:** + +```bash +# macOS: md5 -q src/routes/_index/components/$COMPONENT/manifest.json +# Cross-platform (Node): +node -e "const c=require('crypto'),fs=require('fs');process.stdout.write(c.createHash('md5').update(fs.readFileSync('src/routes/_index/components/$COMPONENT/manifest.json')).digest('hex'))" +``` + +Store this value as `$MANIFEST_HASH` for use in subsequent steps. + +**2. Check if the state file exists:** + +```bash +cat src/routes/_index/components/$COMPONENT/pipeline-state.json 2>/dev/null || echo "NOT_FOUND" +``` + +**3a. If NOT_FOUND — create the state file:** + +Initialize all steps for the resolved scope as `pending` (steps not in this scope +set to `skipped`). See the scope-to-steps table in Step 3. + +> **Before running:** Substitute the actual values of `$COMPONENT`, `$SCOPE`, and `$MANIFEST_HASH` into the command string — these are not shell environment variables that persist across tool calls. + +```bash +node -e " +const fs = require('fs'); +const path = 'src/routes/_index/components/$COMPONENT/pipeline-state.json'; +const now = new Date().toISOString(); +const state = { + component: '$COMPONENT', + scope: '$SCOPE', + manifestHash: '$MANIFEST_HASH', + startedAt: now, + updatedAt: now, + steps: { + '4': { status: 'pending' }, + '5': { status: 'pending' }, + '6': { status: 'pending' }, + '6.5': { status: 'pending' }, + '7': { status: 'pending' }, + 'micro-qa-1': { status: 'pending' }, + '8': { status: 'pending' }, + '9': { status: 'pending' }, + '10': { status: 'pending' }, + '11': { status: 'pending' }, + '12': { status: 'pending' }, + 'micro-qa-2': { status: 'pending' }, + '13': { status: 'pending' }, + '14': { status: 'pending' }, + '15': { status: 'pending' } + } +}; +fs.mkdirSync(require('path').dirname(path), { recursive: true }); +fs.writeFileSync(path, JSON.stringify(state, null, 2)); +console.log('State file created.'); +" +``` + +**3b. If state file exists — validate and resume:** + +Parse the content already read in sub-step 2. Then: + +> If parsing fails (corrupted or partial write), treat this as NOT_FOUND: log a warning `⚠️ pipeline-state.json is corrupt — starting fresh.` and proceed to sub-step 3a to recreate it. + +**(i) Manifest hash check:** + +Compare the stored `manifestHash` against `$MANIFEST_HASH`. If they differ: + +``` +⚠️ Manifest changed since this pipeline run started. + Stored hash: + Current hash: + + Steps already marked complete were generated from a different manifest. + Options: + • Type "reset" to clear the state file and restart from Step 4. + • Type "continue" to proceed — completed steps will NOT be re-run even + though the manifest changed. Use this only if the manifest change is + cosmetic and does not affect the layers already generated. +``` + +Stop and wait. Do not proceed until the engineer responds. + +If "reset": delete `pipeline-state.json` and re-run Step 2.5 as if NOT_FOUND. +If "continue": update `manifestHash` in the state file and proceed normally. + +**(ii) Stall detection:** + +For each step where `status === "in-progress"`, check the `startedAt` timestamp. +If `startedAt` is more than 10 minutes ago relative to now: + +> **Before running:** Substitute the actual values of `$COMPONENT`, `$SCOPE`, and `$MANIFEST_HASH` into the command string — these are not shell environment variables that persist across tool calls. + +```bash +node -e " +const fs = require('fs'); +const path = 'src/routes/_index/components/$COMPONENT/pipeline-state.json'; +const state = JSON.parse(fs.readFileSync(path, 'utf8')); +const now = Date.now(); +const STALL_MS = 10 * 60 * 1000; +Object.entries(state.steps).forEach(([stepId, step]) => { + if (step.status === 'in-progress') { + const age = now - new Date(step.startedAt).getTime(); + if (age > STALL_MS) { + console.log('STALLED:' + stepId); + state.steps[stepId] = { status: 'failed', error: 'Stalled — in-progress for >' + Math.round(age/60000) + ' min. Re-run to retry.' }; + } + } +}); +state.updatedAt = new Date().toISOString(); +fs.writeFileSync(path, JSON.stringify(state, null, 2)); +" +``` + +If any steps were stalled and marked failed, surface them: + +``` +⚠️ Stalled step detected and marked failed: + Step : was in-progress for >10 minutes — treated as failed. + The pipeline will halt at Step . Fix the issue, then + re-run /evo-component $COMPONENT to retry from that step. +``` + +**(iii) Print resume state:** + +After stall detection, print a summary of which steps are complete, which +will be skipped (wrong scope), and which will now run: + +``` +▶ Resuming [scope: ] + ✅ Step 4 — complete (from prior run) + ✅ Step 5 — complete (from prior run) + ⏭ Step 6 — skipped (scope: ) + ▶ Step 7 — will run now (first pending step) + ... +``` + +→ After printing the resume summary, proceed to Step 3 to resolve scope and begin execution from the first pending step. + +### Step marking helpers + +Use these patterns at the start and end of every step execution: + +**Mark step in-progress (before invoking sub-skill):** + +> **Before running:** Substitute the actual values of `$COMPONENT`, `$SCOPE`, and `$MANIFEST_HASH` into the command string — these are not shell environment variables that persist across tool calls. + +```bash +node -e " +const fs = require('fs'); +const p = 'src/routes/_index/components/$COMPONENT/pipeline-state.json'; +const s = JSON.parse(fs.readFileSync(p, 'utf8')); +s.steps[''] = { status: 'in-progress', startedAt: new Date().toISOString() }; +s.updatedAt = new Date().toISOString(); +fs.writeFileSync(p, JSON.stringify(s, null, 2)); +" +``` + +Replace `` with the actual step key string (e.g. `'7'`, `'micro-qa-1'`) before running. + +**Read step status after sub-skill returns (to confirm completion record was written):** + +> **Before running:** Substitute the actual values of `$COMPONENT`, `$SCOPE`, and `$MANIFEST_HASH` into the command string — these are not shell environment variables that persist across tool calls. + +```bash +node -e " +const fs = require('fs'); +const p = 'src/routes/_index/components/$COMPONENT/pipeline-state.json'; +const s = JSON.parse(fs.readFileSync(p, 'utf8')); +const cur = s.steps['']; +console.log(JSON.stringify(cur)); +if (cur.status === 'complete') { + const ORDER = ['4','5','6','6.5','7','micro-qa-1','8','9','10','11','12','micro-qa-2','13','14','15']; + const next = ORDER.find(k => s.steps[k] && s.steps[k].status === 'pending'); + if (next) process.stdout.write('\n⏭ ADVANCE IMMEDIATELY — next pending step: ' + next + '\n'); + else process.stdout.write('\n✅ All steps complete — proceed to State F summary.\n'); +} +" +``` + +Replace `` with the actual step key string (e.g. `'7'`, `'micro-qa-1'`) before running. + +If the returned status is NOT `complete`, the sub-skill did not finish cleanly. +Treat this as a step failure — do NOT advance. + +**Mark step complete (for fast-forward when expected outputs already exist on disk):** + +> **Before running:** Substitute the actual component name for `$COMPONENT` and the actual step key for ``. + +```bash +node -e " +const fs = require('fs'); +const p = 'src/routes/_index/components/$COMPONENT/pipeline-state.json'; +const s = JSON.parse(fs.readFileSync(p, 'utf8')); +s.steps[''] = { status: 'complete', completedAt: new Date().toISOString(), note: 'fast-forwarded — outputs already existed on disk' }; +s.updatedAt = new Date().toISOString(); +fs.writeFileSync(p, JSON.stringify(s, null, 2)); +" +``` + +Replace `` with the actual step key string (e.g. `'4'`, `'6.5'`) before running. + +--- + +## ⛔ Execution rule — deterministic transitions, no pausing between steps + +> **⛔ DO NOT PAUSE BETWEEN STEPS. ⛔** +> When a sub-skill returns, your ONLY action is to read `pipeline-state.json` +> and immediately invoke the next step. Do not summarise, do not ask the +> engineer, do not wait. The sub-skill output is irrelevant — the state file +> is the only signal that matters. + +You are an automated orchestrator. Transitions between steps are determined by +reading `pipeline-state.json` — NOT by evaluating prior output prose. After each +sub-skill returns, you MUST: + +1. Read the state file to confirm the step's status is `complete`. +2. If `complete`: immediately advance to the next step in the transition table. +3. If `failed` or still `in-progress`: halt and surface the failure (see Failure halting below). +4. Never ask the user whether to continue between steps. + +The ONLY valid reasons to stop and wait for input are: + +1. Gate 2 — manifest review (engineer must type "approved") +2. A `failed` step in the state file that cannot be fixed inline +3. Manifest hash mismatch (engineer must type "reset" or "continue") + +**If you find yourself writing a summary or asking "shall I continue?" between +steps, you have violated this rule. Stop writing, read the state file, invoke +the next step.** + +### Transition table + +Read this table as: "after step N is complete, run step M next." +Steps NOT in the scope column for the current scope are marked `skipped` in the state file +and bypassed — the orchestrator does not invoke them. + +| Step | Sub-skill | full | static | interactive | style | +| ---------- | -------------------------- | :--: | :----: | :---------: | :---: | +| 4 | evo-static-component | ✅ | ✅ | ⏭ | ✅ | +| 5 | evo-static-storybook | ✅ | ✅ | ⏭ | ⏭ | +| 6 | evo-docs-hookup (css-only) | ✅ | ✅ | ⏭ | ✅ | +| 6.5 | scaffold generation | ✅ | ⏭ | ✅ | ⏭ | +| 7 | evo-a11y Pass 1 | ✅ | ✅ | ⏭ | ⏭ | +| micro-qa-1 | Micro-QA checkpoint | ✅ | ✅ | ⏭ | ⏭ | +| 8 | evo-marko-component | ✅ | ⏭ | ✅ | ⏭ | +| 9 | evo-marko-storybook | ✅ | ⏭ | ✅ | ⏭ | +| 10 | evo-react-component | ✅ | ⏭ | ✅ | ⏭ | +| 11 | evo-react-storybook | ✅ | ⏭ | ✅ | ⏭ | +| 12 | evo-a11y Pass 2 | ✅ | ⏭ | ✅ | ⏭ | +| micro-qa-2 | Micro-QA checkpoint | ✅ | ⏭ | ✅ | ⏭ | +| 13 | evo-docs-hookup (full) | ✅ | ✅ | ✅ | ⏭ | +| 14 | npm run build | ✅ | ✅ | ✅ | ✅ | +| 15 | evo-qa (Agent spawn) | ✅ | ✅ | ✅ | ✅ | + +### Per-step artifact definitions + +Before running any step, check if its expected outputs already exist on disk. +If they do AND the step is still `pending` in the state file, mark it `complete` +(fast-forward) and advance — do not invoke the sub-skill again. + +| Step | requiredInputs (must exist before running) | expectedOutputs (existence confirms complete) | +| ---- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| 4 | `manifest.json` | `packages/skin/src/sass//.scss` | +| 5 | `packages/skin/src/sass//.scss` | `packages/skin/src/sass//stories/.stories.js` | +| 6 | `manifest.json` | `src/routes/_index/components//css+page.marko` | +| 6.5 | `manifest.json` | `packages/evo-marko/src/tags//index.marko`, `packages/evo-react/src//index.tsx` | +| 7 | `.scss`, `.stories.js` | `src/routes/_index/components//accessibility+page.marko` | +| 8 | `packages/evo-marko/src/tags//index.marko` (scaffold) | `packages/evo-marko/src/tags//index.marko` (complete) | +| 9 | `packages/evo-marko/src/tags//index.marko` | `packages/evo-marko/src/tags//.stories.ts` | +| 10 | `packages/evo-react/src//index.tsx` (scaffold) | `packages/evo-react/src//index.tsx` (complete) | +| 11 | `packages/evo-react/src//index.tsx` | `packages/evo-react/src//.stories.tsx` | +| 12 | `index.marko` (complete), `index.tsx` (complete) | `src/routes/_index/components//accessibility+meta.json` | +| 13 | `manifest.json`, `accessibility+page.marko` | `src/routes/_index/components//+page.marko` | +| 14 | (prior steps complete) | Build exit code 0 | +| 15 | All expected outputs for scope | `pipeline-state.json` step 15 status = complete | + +### Pre-step preamble (run before EVERY step) + +Before marking any step `in-progress` and invoking its sub-skill: + +**1. Check if already complete (idempotent skip):** + +> **Before running:** Substitute the actual component name for `$COMPONENT` and the actual step key for `` — these are not shell variables. + +```bash +node -e " +const fs = require('fs'); +const s = JSON.parse(fs.readFileSync('src/routes/_index/components/$COMPONENT/pipeline-state.json', 'utf8')); +console.log(s.steps[''].status); +" +``` + +If `complete` or `skipped`: print `⏭ Step already complete — skipping.` and advance. + +**2. Validate required inputs exist:** + +For each path in the step's `requiredInputs` list (from the table above): + +```bash +test -f "" && echo "EXISTS" || echo "MISSING: " +``` + +If any required input is MISSING: + +``` +🔴 Pipeline halted — Step cannot start. + Missing required input: + This input should have been produced by Step . + Check Step 's completion record in pipeline-state.json for errors. +``` + +Do not invoke the sub-skill. Mark this step `failed`. Stop. + +**3. Mark step in-progress:** + +> **Before running:** Substitute the actual component name for `$COMPONENT` and the actual step key for ``. + +```bash +node -e " +const fs = require('fs'); +const p = 'src/routes/_index/components/$COMPONENT/pipeline-state.json'; +const s = JSON.parse(fs.readFileSync(p, 'utf8')); +s.steps[''] = { status: 'in-progress', startedAt: new Date().toISOString() }; +s.updatedAt = new Date().toISOString(); +fs.writeFileSync(p, JSON.stringify(s, null, 2)); +" +``` + +Then invoke the sub-skill. + +### Post-step verification (run after EVERY sub-skill returns) + +After the sub-skill returns, run these checks before advancing: + +**1. Read completion record:** + +> **Before running:** Substitute the actual component name for `$COMPONENT` and the actual step key for ``. + +```bash +node -e " +const fs = require('fs'); +const s = JSON.parse(fs.readFileSync('src/routes/_index/components/$COMPONENT/pipeline-state.json', 'utf8')); +console.log(JSON.stringify(s.steps[''], null, 2)); +" +``` + +If status is NOT `complete`: the sub-skill did not finish cleanly. Halt (see Failure halting). + +**2. Content validation (hallucination checks):** + +Run the following checks based on the step just completed: + +_After Step 4 (SCSS generated):_ + +> Substitute the actual BEM block name for `$BLOCK`. + +```bash +# BEM block must exist +grep -c "\.$BLOCK {" packages/skin/src/sass/$BLOCK/$BLOCK.scss +# No deprecated BEM nesting +grep -c "&--" packages/skin/src/sass/$BLOCK/$BLOCK.scss +``` + +The first grep must return ≥ 1. The second must return 0 (nesting forbidden). + +_After Step 8 (Marko generated):_ + +> Substitute the actual BEM block name for `$BLOCK` and the actual tag name (e.g. `evo-accordion`) for `$NAME`. + +```bash +# No Marko 5 scriptlet patterns +grep -c "^\$ \(let\|const\|var\)" packages/evo-marko/src/tags/$NAME/index.marko +# BEM block class must be applied +grep -c "\"$BLOCK\"" packages/evo-marko/src/tags/$NAME/index.marko +``` + +First grep must return 0 (no deprecated patterns); second must return ≥ 1 (BEM class present). + +_After Step 10 (React generated):_ + +> Substitute the actual bare component name (e.g. `accordion`) for `$NAME`. + +```bash +# Component must export a named function or const +grep -c "^export \(function\|const\)" packages/evo-react/src/$NAME/index.tsx +# No forwardRef (evo-react uses React 19 native ref) +grep -c "forwardRef" packages/evo-react/src/$NAME/index.tsx +``` + +First must be ≥ 1; second must be 0. + +If any content check fails: mark the step `failed` with the specific check result as the error. +Halt. Surface to the engineer. + +**3. Scope boundary check:** + +Each step has an `allowedWriteZones` list. After the sub-skill's `outputs` are recorded in +the completion record, verify every listed output path starts with one of these prefixes: + +| Step | allowedWriteZones | +| ---- | ----------------------------------------------------------------------- | +| 4 | `packages/skin/src/sass//`, `packages/skin/src/sass/bundles/` | +| 5 | `packages/skin/src/sass//stories/` | +| 6 | `src/routes/_index/components//` | +| 6.5 | `packages/evo-marko/src/tags//`, `packages/evo-react/src//` | +| 7 | `src/routes/_index/components//` | +| 8 | `packages/evo-marko/src/tags//` | +| 9 | `packages/evo-marko/src/tags//` | +| 10 | `packages/evo-react/src//` | +| 11 | `packages/evo-react/src//` | +| 12 | `src/routes/_index/components//` | +| 13 | `src/routes/_index/components//`, `src/data/` | + +> **Steps 14 and 15 are exempt from scope boundary checking.** Step 14 (`npm run build`) writes +> compiled artefacts to `dist/` directories across multiple packages — this is expected and correct. +> Step 15 (`evo-qa`) writes only to `pipeline-state.json`. Neither produces component output files +> that should be constrained to a write zone. Skip the boundary check for these two steps. + +If any output is outside all allowed zones: + +``` +🔴 Scope boundary violation — Step wrote a file outside its allowed zone: + File: + Allowed zones: + This file was not expected. Review it manually before proceeding. + Type "continue" to accept it or "reset-step" to mark Step failed and retry. +``` + +Wait for engineer input. + +### Failure halting + +When any step ends with status `failed`: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🔴 Pipeline halted — Step () failed +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Error: + +The pipeline state is saved. To retry: +1. Fix the issue described above +2. Re-run /evo-component $COMPONENT --scope $SCOPE + The pipeline will resume from Step . +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +Do not advance to the next step. Do not attempt inline fixes. Stop. + +--- + +## Step 3 — Determine scope and print plan + +If `--scope` was not provided, ask: + +> "What changed in this revision? +> +> - `full` — new component or significant cross-layer changes (default) +> - `static` — HTML structure and/or SCSS changed; framework layers need updating +> - `interactive` — only Marko/React behavior or props changed; static layer untouched +> - `style` — SCSS-only change; no structural or behavioral changes" + +Then print the generation plan: + +``` +Generating $COMPONENT [scope: ] + +Steps that will run: + 4 /evo-static-component [full, static, style(SCSS only)] + 5 /evo-static-storybook [full, static] + 6 /evo-docs-hookup css-only [full, static, style] + 7 /evo-a11y Pass 1 [full, static] — writes static a11y docs + 8 /evo-marko-component [full, interactive] + 9 /evo-marko-storybook [full, interactive] + 10 /evo-react-component [full, interactive] + 11 /evo-react-storybook [full, interactive] + 12 /evo-a11y Pass 2 [full, interactive] — fills interactive a11y docs + 13 /evo-docs-hookup full [full, static, interactive] + 14 npm run build [all scopes] + 15 /evo-qa (forked) [all scopes] + 16 Final summary +``` + +Steps marked ⏭ for this scope are skipped — print them clearly. + +--- + +## Step 4 — Static component + +**Scopes: full, static, style** + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Invoke: `/evo-static-component` (inline) + +- **full / static:** Generate both HTML catalogue and SCSS (SCSS conditional on tokens/figma) +- **style:** Generate SCSS only — HTML structure is unchanged. Tell the skill: "Style scope — regenerate SCSS only. Do not regenerate HTML." + +**Expected output:** + +- HTML catalogue in context (full/static only) +- `packages/skin/src/sass//.scss` (if tokens/figma available) +- `skin-headless.scss` updated (if SCSS generated) + +**style scope:** if no tokens/figma → nothing to do here, print ⏭ and continue to Step 6. + +→ **Next:** After this skill returns, print "Step 4 complete." then immediately invoke Step 5. + +--- + +## Step 5 — Static storybook + +**Scopes: full, static** | ⏭ skip for: interactive, style + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Invoke: `/evo-static-storybook` (inline) + +Reads the HTML catalogue from Step 4 context. Writes CSF2 stories with RTL +and textSpacing exports. + +→ **Next:** After this skill returns, print "Step 5 complete." then immediately invoke Step 6. + +--- + +## Step 6 — CSS docs (css-only) + +**Scopes: full, static, style** | ⏭ skip for: interactive + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Invoke: `/evo-docs-hookup` (inline) with scope `"css-only"` + +Tell the skill: "css-only scope — write only css+page.marko and css+meta.json. +Do not write +page.marko, +meta.json, or update component-metadata.json." + +- **full / static:** HTML catalogue from Step 4 is in context — the skill uses it directly +- **style:** Step 4 ran SCSS-only, so no HTML catalogue is in context. Tell the skill: + "Style scope — no HTML catalogue in context. For any new variants, read + packages/skin/src/sass//stories/.stories.js to get the HTML + for each missing story export, then add the corresponding variant sections + to css+page.marko. Do not rewrite existing sections." + +**Expected output:** + +- `src/routes/_index/components//css+page.marko` +- `src/routes/_index/components//css+meta.json` + +→ **Next:** After this skill returns, print "Step 6 complete." then immediately invoke Step 7 (or Step 6.5 if framework layers are next). + +--- + +## Step 6.5 — Generate component scaffold (deterministic files) + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Run before any framework generation. This produces byte-identical structural files +so the AI skills only need to complete the non-deterministic parts (template body, +component body, behavioral logic): + +```bash +npx tsx scripts/codegen/generate-component-scaffold.ts $COMPONENT +``` + +Files written: + +- `packages/evo-marko/src/tags//style.ts` — complete +- `packages/evo-marko/src/tags//index.marko` — Input interface scaffold + TODO template body +- `packages/evo-marko/src/tags//test/test.server.ts` — complete structure with stubs +- `packages/evo-marko/src/tags//test/test.browser.ts` — stub (if `keyboardModel` present) +- `packages/evo-react/src//index.tsx` — Props interface scaffold + TODO component body + +If the script errors, surface it and stop — do not proceed to framework generation. + +After this step, tell each framework sub-skill: "Scaffold files are already written at +these paths. Read the existing file and complete only the TODO sections — do not +regenerate the Input interface, Props type, style.ts, or test structure." + +→ **Next:** After this script completes, immediately invoke Step 7. + +--- + +## Step 7 — A11y Pass 1 — static layer + writes static a11y docs + +**Scopes: full, static** | ⏭ skip for: interactive, style + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Invoke: `/evo-a11y` (inline) with scope declaration: +`"Pass 1 — static layer only. Steps 4–6 have run in this pipeline run. +Validate the static HTML and static storybook. Write the static sections +of accessibility+page.marko. Do NOT evaluate index.marko or index.tsx."` + +**Pass 1 validates:** + +- ARIA roles and label strategy in the static HTML +- RTL and textSpacing stories in the static storybook +- 🔴 blocking issues stop the pipeline here + +**Pass 1 writes static sections of `accessibility+page.marko`:** + +- Best Practices (from manifest a11y + callerObligations) +- ARIA Reference table (from manifest.a11y.ariaAttributes[]) +- Skeleton placeholders for Keyboard, Screen Reader, Pointer (to be filled by Pass 2) + +If 🔴 blocking issues: stop. Engineer resolves before proceeding. + +→ **Next:** If no 🔴 issues, print "Step 7 complete." then immediately invoke Step 8 (or Step 13 if scope is static). + +--- + +## Micro-QA Checkpoint 1 — Static layer verification (after Step 7) + +**Scopes: full, static** | ⏭ skip for: interactive, style + +> **Before running:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). Use step ID `micro-qa-1`. If already complete, skip to Step 8. + +Spawn a fresh Agent to verify the static layer in genuine isolation. This agent has no +memory of the generation session — it reads only from disk. + +> **Before spawning:** Substitute the actual values for `$COMPONENT`, `$BLOCK`, and `$SCOPE`. + +``` +Agent( + description: "Micro-QA checkpoint 1 — static layer for $COMPONENT", + prompt: """ +You are an isolated QA agent verifying the static layer of an evo-web component pipeline. +You have NO prior context from the generation session. Read ONLY from disk. + +Component: $COMPONENT +BEM block: $BLOCK +Manifest: src/routes/_index/components/$COMPONENT/manifest.json + +Read the manifest first. Then run each check below in order. + +=== CHECKS === + +CHECK 1 — SCSS file exists and is non-empty +Command: test -s packages/skin/src/sass/$BLOCK/$BLOCK.scss && echo PASS || echo FAIL + +CHECK 2 — BEM block rule present in SCSS +Command: grep -c ".$BLOCK {" packages/skin/src/sass/$BLOCK/$BLOCK.scss +Pass: count >= 1. Fail: count is 0. + +CHECK 3 — No deprecated BEM nesting in SCSS +Command: grep -c "&--" packages/skin/src/sass/$BLOCK/$BLOCK.scss +Pass: count is 0. Fail: any nesting found. + +CHECK 4 — Every modifier from manifest.bem.modifiers[] has a rule in SCSS +For each modifier name M in manifest.bem.modifiers[]: + grep -c ".$BLOCK--M " packages/skin/src/sass/$BLOCK/$BLOCK.scss + Pass: count >= 1. Fail: count is 0 (rule missing). + +CHECK 5 — Stories file exists +Command: test -f packages/skin/src/sass/$BLOCK/stories/$BLOCK.stories.js && echo PASS || echo FAIL + +CHECK 6 — RTL export present in stories +Command: grep -c "export.*RTL" packages/skin/src/sass/$BLOCK/stories/$BLOCK.stories.js +Pass: count >= 1. + +CHECK 7 — textSpacing export present in stories +Command: grep -c "export.*textSpacing" packages/skin/src/sass/$BLOCK/stories/$BLOCK.stories.js +Pass: count >= 1. + +CHECK 8 — accessibility+page.marko exists +Command: test -f src/routes/_index/components/$COMPONENT/accessibility+page.marko && echo PASS || echo FAIL + +CHECK 9 — No Marko 5 scriptlet patterns in SCSS directory +Command: grep -rn "^\$ \(let\|const\|var\)" packages/skin/src/sass/$BLOCK/ +Pass: no output. Fail: any match. + +=== WRITE RESULT === + +After all checks, write your result to the pipeline state file. +Substitute the actual value of $COMPONENT before running: + +node -e " +const fs = require('fs'); +const p = 'src/routes/_index/components/$COMPONENT/pipeline-state.json'; +const s = JSON.parse(fs.readFileSync(p, 'utf8')); +const issues = []; +// populate issues[] with any FAIL results from your checks above (as strings) +s.steps['micro-qa-1'] = { + status: issues.length === 0 ? 'complete' : 'failed', + completedAt: new Date().toISOString(), + checks: 9, + issues +}; +s.updatedAt = new Date().toISOString(); +fs.writeFileSync(p, JSON.stringify(s, null, 2)); +" + +Return a JSON object: { passed: boolean, issues: string[] } +If passed is false, list each failing check with the specific file and what was expected. + """ +) +``` + +After the Agent returns, read the micro-qa-1 result from the state file: + +> **Before running:** Substitute the actual value of `$COMPONENT`. + +```bash +node -e " +const fs = require('fs'); +const s = JSON.parse(fs.readFileSync('src/routes/_index/components/$COMPONENT/pipeline-state.json', 'utf8')); +console.log(JSON.stringify(s.steps['micro-qa-1'], null, 2)); +" +``` + +**If micro-qa-1 status is `failed`:** + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🔴 Micro-QA Checkpoint 1 FAILED — static layer issues detected +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +The following issues were found by an isolated verification agent: + + +These issues were caught BEFORE the Marko and React layers were generated, +saving you from building framework layers on a broken static foundation. + +Fix the issues above by re-running /evo-static-component or editing files +manually, then re-run /evo-component $COMPONENT to retry from this checkpoint. +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +Do NOT advance to Step 8. Do not attempt inline fixes. Stop. + +**If micro-qa-1 status is `complete`:** + +``` +✅ Micro-QA Checkpoint 1 — static layer verified by isolated agent (9/9 checks passed) +``` + +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). Use step ID `micro-qa-1`. + +→ **Next:** After micro-qa-1 passes, immediately invoke Step 8. + +--- + +## Step 8 — Marko component + +**Scopes: full, interactive** | ⏭ skip for: static, style + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Invoke: `/evo-marko-component` (inline) + +Reads static HTML from context (Step 4, full scope) or from existing storybook +file (interactive scope — no Step 4 was run). Generates Marko 6 component. + +**Expected output** (`packages/evo-marko/src/tags//`): + +- `index.marko`, `style.ts`, `test/test.server.ts`, `test/test.browser.ts` (if interactive) + +→ **Next:** After this skill returns, print "Step 8 complete." then immediately invoke Step 9. + +--- + +## Step 9 — Marko storybook + +**Scopes: full, interactive** | ⏭ skip for: static, style + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Invoke: `/evo-marko-storybook` (inline) + +**Expected output:** + +- `packages/evo-marko/src/tags//.stories.ts` + `examples/` + +→ **Next:** After this skill returns, print "Step 9 complete." then immediately invoke Step 10. + +--- + +## Step 10 — React component + +**Scopes: full, interactive** | ⏭ skip for: static, style + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Invoke: `/evo-react-component` (inline) + +**Expected output** (`packages/evo-react/src//`): + +- `index.tsx`, `__tests__/index.spec.tsx` + +→ **Next:** After this skill returns, print "Step 10 complete." then immediately invoke Step 11. + +--- + +## Step 11 — React storybook + +**Scopes: full, interactive** | ⏭ skip for: static, style + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Invoke: `/evo-react-storybook` (inline) + +**Expected output:** + +- `packages/evo-react/src//.stories.tsx` + +→ **Next:** After this skill returns, print "Step 11 complete." then immediately invoke Step 12. + +--- + +## Step 12 — A11y Pass 2 — full validation + fills interactive a11y docs + +**Scopes: full, interactive** | ⏭ skip for: static, style + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Invoke: `/evo-a11y` (inline) with scope declaration: +`"Pass 2 — full validation. Steps 4–11 have run in this pipeline run +(or Steps 8–11 for interactive scope). Validate all layers. Fill in the +interactive sections of accessibility+page.marko."` + +**Pass 2 validates:** + +- Static HTML (re-check) +- `index.marko` — ARIA wiring, keyboard handlers, Marko 6 syntax +- `index.tsx` — ARIA wiring, keyboard handlers +- Marko storybook — interactive story if keyboardModel present +- React storybook — controlled story if keyboardModel present + +**Pass 2 fills in interactive sections of `accessibility+page.marko`:** + +- Keyboard section (from manifest.keyboardModel + manifest.keyboardInteractions[]) +- Screen Reader section (from manifest.a11y.screenReaderAnnouncement — interactive states) +- Pointer section (active/interactive behavior) +- Also writes `accessibility+meta.json` (full picture now available) + +**For `--scope interactive`:** The accessibility+page.marko may already have +static sections from a previous run. Fills in/updates only the interactive +sections without overwriting the static sections. + +If 🔴 blocking issues: fix inline before build. + +→ **Next:** If no 🔴 issues, print "Step 12 complete." then immediately invoke Step 13. + +--- + +## Micro-QA Checkpoint 2 — Framework layer verification (after Step 12) + +**Scopes: full, interactive** | ⏭ skip for: static, style + +> **Before running:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). Use step ID `micro-qa-2`. If already complete, skip to Step 13. + +Spawn a fresh Agent to verify the framework layer in genuine isolation. This agent has no +memory of the generation session — it reads only from disk. + +> **Before spawning:** Substitute the actual values for `$COMPONENT`, `$BLOCK`, `$MARKO_NAME` (e.g. `evo-accordion`), `$REACT_NAME` (bare kebab-case, e.g. `accordion`), and `$SCOPE`. + +``` +Agent( + description: "Micro-QA checkpoint 2 — framework layer for $COMPONENT", + prompt: """ +You are an isolated QA agent verifying the framework layer of an evo-web component pipeline. +You have NO prior context from the generation session. Read ONLY from disk. + +Component: $COMPONENT +BEM block: $BLOCK +Marko name: $MARKO_NAME (e.g. evo-accordion) +React name: $REACT_NAME (e.g. accordion) +Manifest: src/routes/_index/components/$COMPONENT/manifest.json + +Read the manifest fully before starting checks. + +=== CHECKS === + +CHECK 1 — index.marko exists and is non-empty +Command: test -s packages/evo-marko/src/tags/$MARKO_NAME/index.marko && echo PASS || echo FAIL + +CHECK 2 — No Marko 5 scriptlet patterns in index.marko +Command: grep -cn "^\$ \(let\|const\|var\)" packages/evo-marko/src/tags/$MARKO_NAME/index.marko +Pass: 0. Fail: any count > 0 (list each matching line). + +CHECK 3 — BEM block class applied in index.marko +Command: grep -c "\"$BLOCK\"" packages/evo-marko/src/tags/$MARKO_NAME/index.marko +Pass: >= 1. + +CHECK 4 — Every prop in manifest.props[] appears in the Input interface +For each prop name P in manifest.props[]: + grep -c "P[?]?:" packages/evo-marko/src/tags/$MARKO_NAME/index.marko + Pass: >= 1. Fail: 0 (prop missing from interface). + +CHECK 5 — style.ts contains correct skin import +Command: cat packages/evo-marko/src/tags/$MARKO_NAME/style.ts +Expected content: import "@ebay/skin/$BLOCK"; +Pass: exact match. Fail: wrong or missing. + +CHECK 6 — index.tsx exists and is non-empty +Command: test -s packages/evo-react/src/$REACT_NAME/index.tsx && echo PASS || echo FAIL + +CHECK 7 — No forwardRef in index.tsx (evo-react uses React 19 native ref) +Command: grep -c "forwardRef" packages/evo-react/src/$REACT_NAME/index.tsx +Pass: 0. Fail: any count > 0. + +CHECK 8 — Component export present in index.tsx +Command: grep -c "^export \(function\|const\)" packages/evo-react/src/$REACT_NAME/index.tsx +Pass: >= 1. + +CHECK 9 — Every prop in manifest.props[] appears in React Props type +For each prop name P in manifest.props[]: + grep -c "P[?]?:" packages/evo-react/src/$REACT_NAME/index.tsx + Pass: >= 1. + +CHECK 10 — Marko stories file exists +Command: test -f packages/evo-marko/src/tags/$MARKO_NAME/$MARKO_NAME.stories.ts && echo PASS || echo FAIL + +CHECK 11 — React stories file exists +Command: find packages/evo-react/src/$REACT_NAME -name "*.stories.tsx" | grep -c . +Pass: >= 1. + +CHECK 12 — accessibility+meta.json exists +Command: test -f src/routes/_index/components/$COMPONENT/accessibility+meta.json && echo PASS || echo FAIL + +=== WRITE RESULT === + +After all checks, write your result to the pipeline state file. +Substitute the actual value of $COMPONENT before running: + +node -e " +const fs = require('fs'); +const p = 'src/routes/_index/components/$COMPONENT/pipeline-state.json'; +const s = JSON.parse(fs.readFileSync(p, 'utf8')); +const issues = []; +// populate with any FAIL results as strings +s.steps['micro-qa-2'] = { + status: issues.length === 0 ? 'complete' : 'failed', + completedAt: new Date().toISOString(), + checks: 12, + issues +}; +s.updatedAt = new Date().toISOString(); +fs.writeFileSync(p, JSON.stringify(s, null, 2)); +" + +Return: { passed: boolean, issues: string[] } + """ +) +``` + +After the Agent returns, read micro-qa-2 from the state file: + +> **Before running:** Substitute the actual value of `$COMPONENT`. + +```bash +node -e " +const fs = require('fs'); +const s = JSON.parse(fs.readFileSync('src/routes/_index/components/$COMPONENT/pipeline-state.json', 'utf8')); +console.log(JSON.stringify(s.steps['micro-qa-2'], null, 2)); +" +``` + +**If micro-qa-2 status is `failed`:** + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🔴 Micro-QA Checkpoint 2 FAILED — framework layer issues detected +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +The following issues were found by an isolated verification agent: + + +These issues were caught before docs, build, and final QA run. +Fix the issues, then re-run /evo-component $COMPONENT to retry from this checkpoint. +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +Do NOT advance to Step 13. Do not attempt inline fixes. Stop. + +**If micro-qa-2 status is `complete`:** + +``` +✅ Micro-QA Checkpoint 2 — framework layer verified by isolated agent (12/12 checks passed) +``` + +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). Use step ID `micro-qa-2`. + +→ **Next:** After micro-qa-2 passes, immediately invoke Step 13. + +--- + +## Step 13 — Docs hookup (full) + +**Scopes: full, static, interactive** | ⏭ skip for: style + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Invoke: `/evo-docs-hookup` (inline) with scope `"full"` + +Tell the skill: "full scope — write +page.marko, +meta.json, and update +component-metadata.json. Do NOT rewrite css+page.marko (already done in Step 6)." + +**Expected output:** + +- `src/routes/_index/components//+page.marko` +- `src/routes/_index/components//+meta.json` +- `src/data/component-metadata.json` entry added/updated + +→ **Next:** After this skill returns, print "Step 13 complete." then immediately run Step 14 (npm run build). + +--- + +## Step 14 — Build validation + +**Scopes: all** + +> **Before invoking:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). + +Run: `npm run build` + +Fix failures inline. Do not advance to QA with a failing build. + +→ **Next:** After the build passes, immediately invoke Step 15. + +--- + +## Step 15 — QA + +**Scopes: full, static, interactive, style** (always runs) + +> **Before running:** Run the pre-step preamble above (idempotent check → pre-flight validation → mark in-progress). Use step ID `15`. If already complete, skip. + +Spawn a fresh Agent with subagent_type `evo-qa` to run QA in genuine isolation. +The evo-qa agent has no memory of the generation session — it reads only from disk. + +> **Before spawning:** Substitute the actual values for `$COMPONENT`, `$SCOPE`, and `$BLOCK`. +> For `files`, read `steps[*].outputs` from `pipeline-state.json` and flatten into a list. +> For `reference`, use the manifest's `migration.legacyName` field if present; otherwise omit. + +``` +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 files = Object.values(s.steps) + .filter(st => st && st.outputs) + .flatMap(st => st.outputs); +console.log(JSON.stringify(files, null, 2)); +" +``` + +Then spawn the QA agent: + +``` +Agent( + subagent_type: "evo-qa", + description: "QA verification for $COMPONENT (scope: $SCOPE)", + prompt: """ +manifest: src/routes/_index/components/$COMPONENT/manifest.json +files: [] +reference: +scope: $SCOPE + """ +) +``` + +After the Agent returns, read its output. If it contains `Layer 1 result: ✅ PASSED`, write the completion record below with `status: "complete"`. If it contains `Layer 1 result: 🔴 FAILED`, write it with `status: "failed"` and surface the QA report to the engineer. + +**If QA passed:** + +> **Before running:** Substitute the actual value of `$COMPONENT`. + +```bash +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')); +s.steps['15'] = { + status: 'complete', + completedAt: new Date().toISOString(), + outputs: [] +}; +s.updatedAt = new Date().toISOString(); +fs.writeFileSync(p, JSON.stringify(s, null, 2)); +console.log('Step 15 completion record written.'); +" +``` + +**If QA failed:** + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🔴 QA FAILED — isolated verification agent found issues +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +> **Before running:** Substitute the actual value of `$COMPONENT` and describe the failure. + +```bash +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')); +s.steps['15'] = { + status: 'failed', + error: '' +}; +s.updatedAt = new Date().toISOString(); +fs.writeFileSync(p, JSON.stringify(s, null, 2)); +" +``` + +Do NOT mark the pipeline complete when QA fails. Surface the report and stop. + +> **After returning:** Run the post-step verification above (read completion record → content validation → scope boundary check). Use step ID `15`. + +→ **Next:** Pipeline complete. Print the final summary banner: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +✅ Pipeline complete: $COMPONENT ($SCOPE scope) +All steps verified. QA passed. +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +--- + +## Step 16 — Final summary + +``` +## Component generated: $COMPONENT [scope: ] + +Static layer: + [✅ | ⏭] /evo-static-component — HTML + SCSS + [✅ | ⏭] /evo-static-storybook — CSF2 stories + [✅ | ⏭] css+page.marko written + [✅ | ⏭] /evo-a11y Pass 1 — static a11y docs written + +Marko layer: + [✅ | ⏭] index.marko + style.ts + [✅ | ⏭] Marko storybook + +React layer: + [✅ | ⏭] index.tsx + [✅ | ⏭] React storybook + +A11y Pass 2: [✅ | ⏭] Interactive sections filled +Docs (full): [✅ | ⏭] Overview + component-metadata.json +Build: ✅ Passed +QA L1: ✅ Passed (or: 🔴 N failure(s) — list each) +QA L2: ✅ N/A (or: fidelity score + delta) + +───────────────────────────────────────────────── +Next steps: +[scope: style] 1. Review SCSS output. File PR. /evo-release-workflow +[scope: static] 1. Review static layer + a11y docs. + 2. Run /evo-component --scope interactive if framework needs updating. + 3. File PR. /evo-release-workflow +[scope: full] 1. Review generated files and resolve any 🟡 warnings. + 2. Fill storybook paths in component-metadata.json after first deploy. + 3. File PR. /evo-release-workflow +───────────────────────────────────────────────── +``` + +--- + +## Scope reference + +| Scope | When to use | Steps run | +| ------------- | --------------------------------------------- | ----------------- | +| `full` | New component; cross-layer changes | 4–16 | +| `static` | HTML structure and/or SCSS changed | 4–7, 13–16 | +| `interactive` | Only Marko/React behavior or props changed | 6.5, 8–16 | +| `style` | SCSS only; no structural or behavioral change | 4(SCSS), 6, 14–16 | + +> **Note on Step 6.5 and interactive scope:** Step 6.5 (scaffold generation) runs for interactive scope +> because it produces the `index.marko` and `index.tsx` scaffold files that Steps 8 and 10 complete. +> The "8–16" shorthand in the table above is updated to "6.5, 8–16" to reflect this dependency. + +## Sub-skill scope declarations + +All sub-skills run inline. The orchestrator explicitly tells each skill what +scope this invocation is running — never infer from disk state. + +| Sub-skill | Context | Scope declaration pattern | +| ---------------------------- | ---------- | ------------------------------------------------------------------------------------- | +| `/evo-static-component` | Inline | "full/static scope: full HTML + SCSS" or "style scope: SCSS only" | +| `/evo-static-storybook` | Inline | (no scope needed — always reads Step 4 HTML) | +| `/evo-docs-hookup` (Step 6) | Inline | "css-only scope" | +| `/evo-a11y` Pass 1 | Inline | "Pass 1 — static layer only; steps 4–6 ran" | +| `/evo-marko-component` | Inline | (reads static HTML from context or existing storybook) | +| `/evo-marko-storybook` | Inline | (no scope needed) | +| `/evo-react-component` | Inline | (reads static HTML from context or existing storybook) | +| `/evo-react-storybook` | Inline | (no scope needed) | +| `/evo-a11y` Pass 2 | Inline | "Pass 2 — full scope; steps 4–11 ran" or "Pass 2 — interactive scope; steps 8–11 ran" | +| `/evo-docs-hookup` (Step 13) | Inline | "full scope — Overview + metadata only, css+page.marko already written" | +| `/evo-qa` | **Forked** | Told: scope used, which files were generated this run | diff --git a/.claude/skills/evo-component/evals/evals.json b/.claude/skills/evo-component/evals/evals.json new file mode 100644 index 0000000000..bf8295c8d3 --- /dev/null +++ b/.claude/skills/evo-component/evals/evals.json @@ -0,0 +1,80 @@ +{ + "skill_name": "evo-component", + "evals": [ + { + "id": 1, + "name": "missing-manifest", + "prompt": "Run /evo-component evo-nonexistent-widget in the evo-web repo at /Users/arkhachatryan/Repos/evo-web. There is no manifest.json for this component. Save the full text output (what Claude prints/decides) to response.md in the outputs directory.", + "expected_output": "A clear error message saying the manifest was not found and directing the user to run /evo-create-component-manifest first. No files should be written.", + "files": [], + "assertions": [ + { + "name": "clear-error-for-missing-manifest", + "description": "Output contains an explicit error message stating that no manifest was found at the expected path." + }, + { + "name": "redirects-to-manifest-skill", + "description": "Output directs user to run /evo-create-component-manifest before /evo-component." + }, + { + "name": "no-files-written", + "description": "Output explicitly states no files were written, or no file-writing actions occurred." + }, + { + "name": "mentions-correct-path", + "description": "Output references the specific path contracts/evo-nonexistent-widget/manifest.json." + } + ] + }, + { + "id": 2, + "name": "blocking-gaps-gate", + "prompt": "Run /evo-component evo-avatar in the evo-web repo at /Users/arkhachatryan/Repos/evo-web. The manifest exists at contracts/avatar/manifest.json and has low-confidence blocking gaps (component.designSystemVersion, states[loading].cssSelector, states[imageError].cssSelector). Save the full text output to response.md in the outputs directory.", + "expected_output": "A GATE BLOCKED message listing the specific low-confidence fields that must be resolved, with no files written and no code generation begun.", + "files": ["contracts/avatar/manifest.json"], + "assertions": [ + { + "name": "gate-blocked-message", + "description": "Output contains a 'GATE BLOCKED' header or equivalent clear gate stop signal." + }, + { + "name": "lists-specific-gap-fields", + "description": "Output enumerates specific blocking field paths from the manifest gaps array (e.g. component.designSystemVersion, states[loading].cssSelector)." + }, + { + "name": "no-code-generation-started", + "description": "Output contains no Marko, React, or SCSS code, and does not mention invoking evo-marko-migration or evo-react-migration." + }, + { + "name": "instructs-how-to-fix", + "description": "Output tells the user how to resolve the blocking gaps (edit manifest.json) and re-run." + } + ] + }, + { + "id": 3, + "name": "pr1-build-path", + "prompt": "Run /evo-component evo-test-clean in the evo-web repo at /Users/arkhachatryan/Repos/evo-web. The manifest exists at contracts/evo-test-clean/manifest.json, has no blocking gaps (only medium-confidence gaps), and figmaUrl is null. Save the full text output to response.md in the outputs directory. Stop after the skill announces the build path and begins calling sub-skills (it's OK if it can't actually invoke them since they don't exist yet).", + "expected_output": "The skill announces 'PR 1 build (no Figma yet)', correctly identifies the build path as Phase 1 + Phase 3 (no Phase 2), and then attempts to call the Marko sub-skill first.", + "files": ["contracts/evo-test-clean/manifest.json"], + "assertions": [ + { + "name": "announces-pr1-not-full-build", + "description": "Output explicitly announces 'PR 1 build' (not 'Full build') since figmaUrl is null." + }, + { + "name": "phase2-skipped-with-explanation", + "description": "Output mentions that Phase 2 (visual SCSS / evo-style-component) is skipped because figmaUrl is null, and explains how to trigger it later." + }, + { + "name": "marko-invoked-before-react", + "description": "Output shows evo-marko-migration is invoked (or attempted) before evo-react-migration." + }, + { + "name": "storybook-phase-included", + "description": "Output mentions Phase 3 storybook steps will run (even if deferred due to missing sub-skills)." + } + ] + } + ] +} diff --git a/.claude/skills/evo-create-component-manifest/SKILL.md b/.claude/skills/evo-create-component-manifest/SKILL.md new file mode 100644 index 0000000000..8395ba4a3a --- /dev/null +++ b/.claude/skills/evo-create-component-manifest/SKILL.md @@ -0,0 +1,455 @@ +--- +name: evo-create-component-manifest +description: > + Pipeline entry point for evo-web AI component generation. Reads an approved + component contract (`src/routes/_index/components/[component]/_contract.md`) + and produces two structured files: `manifest.json` (machine-readable spec for + downstream code generation) and `gap-report.json` (a full log of every + assumption made). Invoke this skill whenever the user runs + `/evo-create-component-manifest`, says "create manifest for [component]", + "generate manifest from contract", "run the manifest step", or "kick off the + pipeline for [component]". Do NOT wait for the user to spell out all of these + phrases — if they're asking to start the component pipeline or convert a + contract to a manifest, this is the skill to use. +--- + +# Create Component Manifest + +This skill is the **first and only human gate before code generation begins**. +Its value is rigor: every ambiguity in the contract must surface here, in a +form the engineer can read and resolve, before a single line of SCSS or React +is written. Silently filling in gaps or skipping uncertain fields defeats the +entire purpose. + +**Scope: read contract → write two files → surface gaps → stop.** + +Do NOT generate component code (no SCSS, Marko, React, JS). +Do NOT invoke `/evo-component` or any downstream skill. +Do NOT silently populate fields you are not certain about — log the gap instead. + +See `references/manifest-schema.md` for the full annotated schema and real component +examples. See `references/contract-template-additions.md` for what the design contract +should supply. + +--- + +## Step 1 — Parse the component name + +The component name is `$ARGUMENTS` (e.g. `ebay-avatar`). + +If `$ARGUMENTS` is empty, ask: +> "Which component should I generate a manifest for? (e.g. `ebay-avatar`)" + +Then wait. Do not proceed until you have a component name. + +--- + +## Step 2 — Read the contract + +Read `src/routes/_index/components/$COMPONENT/_contract.md` in full before writing anything. + +If the file does not exist, stop and report: + +``` +ERROR: No contract found at src/routes/_index/components/$COMPONENT/_contract.md + +Create the contract first using the _contract.md template. +No manifest was generated. +``` + +--- + +## Step 2a — Run spec-to-manifest script (if spec present), then read + +Check for `src/routes/_index/components/$COMPONENT/*.spec.json` (any file matching that glob). + +**If found:** First run the deterministic translation script before doing any AI inference. +This writes the spec-derived fields (props, tokens, slots, states, figma) into `manifest.json` +with byte-identical precision, so you do not need to infer those fields: + +```bash +npx tsx scripts/codegen/spec-to-manifest.ts $COMPONENT +``` + +If the script succeeds, `manifest.json` now has the spec-derived sections pre-filled. +Then read the updated `manifest.json` and the spec file (`$SPEC`) for your remaining work. +Only handle the **contract-sourced fields** in your AI inference step — do not re-derive +props, tokens, slots, states, or figma.fileKey from the spec yourself (the script already did this). + +**If the script fails or no spec is found:** Continue with AI inference for all fields. + +Read the spec in full and store it as `$SPEC`. This is a machine-readable +handoff from the design team. It is the **authoritative source** for the fields it +covers — treat spec-sourced values as `confidence: high, source: spec` with no gap entry. + +`$SPEC` fields and how they map: + +| Spec field | Manifest field | Translation | +|---|---|---| +| `props.*` | `props[]` | Each key → prop object; `enum` type → values array | +| `slots.*` | `slots[]` | Each key → slot with `required` flag | +| `states.state[]` | `states[]` | Direct array of state names | +| `tokens.*` | `tokens` | Replace `.` with `-`, prepend `--` (e.g. `color.background.primary` → `--color-background-primary`) | +| `a11y.role` | `a11y.role` | Direct | +| `a11y.aria.*` | `a11y.ariaAttributes[]` | Each key-value → attribute entry | +| `metadata.figma.fileKey` | `figma.fileKey` | Store as-is alongside any `figmaUrl` from contract | +| `description` | `component.description` | Use if contract description is absent | + +**Spec does NOT replace:** +- `a11yProps` — these are i18n strings, not in the spec +- `bem` — structural concern, from audit +- `behaviors`, `events`, `keyboardModel`, `callerObligations` — engineering/contract concerns +- `figmaUrl` — the spec only has `fileKey`; full URL (with nodeId) comes from the contract + +**If not found:** Continue. All fields fall back to contract inference and audit. + +--- + +## Step 2b — Load the audit snapshot + +Check for `scripts/audit-output/components/$COMPONENT.json`. + +**If it exists:** Read it in full and store it as `$AUDIT`. You will use it to fill +`[AUDIT]`-tagged fields directly — no inference, no gap entry needed when data is present. + +**If it does not exist** (brand-new component not yet in the codebase): +- All `[AUDIT]` fields become soft gaps: `confidence: medium, source: missing` +- Add a single note at the top of the gap report: + `"Component not in audit snapshot. Run component-audit.js after the skin module exists to auto-fill BEM, tokens, dependencies, and ARIA attributes."` +- Do not let this block progress — proceed with contract-sourced fields only. + +--- + +## Step 3 — Extract fields + +Work through the manifest structure field by field. For each field, apply this +sourcing priority: + +| Source | Action | Gap entry? | +|---|---|---| +| `$SPEC` — present and explicit | Use it; `confidence: high, source: spec` | No | +| `$AUDIT` — found in snapshot | Use it; `confidence: high, source: audit` | No | +| Contract — clearly and explicitly stated | Use it verbatim | No | +| Contract — implied, inferable | Use inferred value | Yes — `confidence: medium, source: inferred` | +| Not mentioned anywhere | Set to `null` | Yes — `confidence: low, source: missing` | + +Every populated field must be traceable to a source. Every `null` must have +an explanation in the gap report. If you infer from domain knowledge alone (not +from the contract or spec), log a gap regardless of how obvious the value seems. + +### Fields to extract + +Work through each section in order. Fields tagged **[CONTRACT]** should be +extractable from the contract. Fields tagged **[ENGINEER]** are expected to be +`null` with a gap entry — the engineer fills these in during GATE 2. +Fields tagged **[INFER]** can be attempted with medium confidence. + +**Component identity** [CONTRACT; `description` also from SPEC] +- `component.name` — kebab-case tag name (e.g. `evo-button`) +- `component.displayName` — human name (e.g. `Button`) +- `component.description` — one-sentence purpose; prefer `$SPEC.description` if present +- `component.category` — UI category (e.g. `buttons`, `graphics & icons`, `form`) +- `figmaUrl` — Figma design link from contract (full URL with nodeId); null if not in contract +- `figma.fileKey` — from `$SPEC.metadata.figma.fileKey` if present; stored alongside figmaUrl + +**Root element** [INFER] +- `rootElement.default` — the primary HTML element (button, div, a, span, dialog, etc.) +- `rootElement.conditional` — if the element can change based on a prop, capture the condition and alternate element +- `rootElement.passthroughAttributes` — which native attributes pass through to the root +- `rootElement.excludedAttributes` — which attributes are managed internally and must not be passed through + +**Props** [SPEC when present, otherwise CONTRACT for existence/purpose, ENGINEER for types/defaults] +- If `$SPEC.props` exists: use it as the authoritative source. Each spec prop has `type`, + `enum` (values list), `default`, and `description` — extract all directly, no gap needed. +- If no spec: for each prop: `name`, `type`, `values` (enum only), `default`, `required`, `description` +- Do not guess types without spec. If the contract only mentions a prop exists, log its type and default as gaps. + +**A11y props** [CONTRACT] — CRITICAL, often missing from contracts +- These are i18n-able text strings consumers must override for localization (e.g. `a11yText`, `a11yLoadingText`, `a11ySelectedText`) +- For each: `name`, `type`, `default` (English string), `required`, `allowNull`, `nullMeaning`, `condition`, `offscreenMethod`, `appendsToVisible`, `description` +- If the contract's "Required label strings" section names these → extract them +- If the contract is silent → add a 🔴 gap. Every interactive or announced component needs at least one a11y prop. + +**Slots** [SPEC when present, otherwise CONTRACT] +- If `$SPEC.slots` exists: use it as the base. Each spec slot has `required` and `description`. + Infer `type` (default | named-attrtag | named) from the slot's role described in the contract — + spec does not provide the Marko slot type, so that still requires contract/engineering judgement. +- Named content regions: `name`, `type` (default | named-attrtag | named), `required`, `requiredReason` (when required: true), `description` +- Also check: `elementTypeOverride` (slot accepts `as` prop to change element), `decorative` (CSS background only), `pinned` (outside scrollable area) +- Marko's named AttrTag slots (`<@image>`) are a distinct pattern — call out the type explicitly + +**Nested slots** [CONTRACT/ENGINEER] +- When a slot's definition itself contains named sub-slots (e.g. evo-tabs' `tab` slot contains a `panel` sub-slot) +- Populate `nestedSlots[]` with `parentSlot`, `childSlots[]`, `description` +- Log as gap if not mentioned in contract — this is a non-obvious structural pattern + +**Slot a11y props** [CONTRACT/ENGINEER] +- When a slot's interface includes an a11y text string (e.g. postfixIcon.a11yText becomes aria-label on the inner button) +- Populate `slotA11yProps[]` with `slot`, `prop`, `appliedAs`, `description` + +**Variants** [CONTRACT] +- Distinct rendering modes: `name`, `trigger` (which prop/condition activates), `description` + +**States** [SPEC when present, otherwise CONTRACT] +- If `$SPEC.states.state[]` exists: use the array as the list of state names. + `trigger`, `cssSelector`, `ariaAttribute`, `keyboardAccess`, `renderChange` still come from contract/engineering. +- Visual or ARIA states: `name`, `trigger`, `cssSelector` [ENGINEER], `ariaAttribute` [ENGINEER], `keyboardAccess`, `renderChange`, `description` + +**A11y** [CONTRACT→ENGINEER translation] +Extract from the contract's Accessibility section and translate to technical fields: +- `role` — ARIA role if a non-semantic element is used; null if a semantic element handles it +- `explicitRole` — true if `role=""` must be added as an attribute +- `labelStrategy` — `content` | `aria-label-prop` | `aria-labelledby` | `aria-hidden` | `compound-labelledby` +- `focusable` — is this component keyboard focusable? +- `focusableWhenDisabled` — `always` | `partiallyDisabled-only` | `never` +- `tabOrder` — `natural` | `manual` | `none` +- `focusTrap` — `browser-native` | `javascript` | `none` [ENGINEER] +- `initialFocus` — where focus lands when component opens [CONTRACT — look for "Open/close rules"] +- `focusReturn` — where focus returns on close [CONTRACT — often a caller obligation] +- `screenReaderAnnouncement` — plain English: what does a screen reader announce? [CONTRACT] +- `ariaAttributeOwnership.managed` — aria-* attrs managed internally; caller must not set [ENGINEER] +- `ariaAttributeOwnership.passthrough` — aria-* attrs the caller can set [ENGINEER] +- `ariaAttributes[]` — specific aria-* attributes with their values and conditions [ENGINEER] + +**Widget role** [CONTRACT] — for interactive widget components only +- If the contract's "Widget type" section names a WAI-ARIA pattern (tabs, menu, listbox, combobox): + - `widgetRole.containerRole` — role on the host element (tablist, menu, listbox) + - `widgetRole.itemRole` — default role on child items (tab, menuitem, option) + - `widgetRole.itemRoleVariants[]` — when item role varies at runtime (e.g. menuitem vs menuitemradio) + - `widgetRole.supportingRoles[]` — additional roles (tabpanel, separator) + - `widgetRole.relatedRoles` — cross-element ARIA relationships (aria-controls, aria-selected, aria-checked, aria-expanded) +- NOTE: listbox uses `aria-selected`; menu uses `aria-checked` — do not confuse these +- Log as gap if contract does not declare widget type + +**Keyboard model** [CONTRACT] — for widget components only +- If widgetRole is populated, also populate keyboardModel: + - `focusStrategy` — `roving-tabindex` | `aria-activedescendant` | `none` + - Decision: roving-tabindex for menu/tabs/listbox; aria-activedescendant for combobox (textbox must retain focus) + - `focusStrategyReason` — brief rationale + - `autoSelect.supported`, `autoSelect.prop`, `autoSelect.description` + - `wraps` — does navigation wrap at list ends? + - `keys[]` — non-arrow-key interactions (Escape, Home, End) + - `typeahead` — if type-ahead search is supported +- Log as gap if not described in contract + +**State-lifting callbacks** [CONTRACT] — SEPARATE from events +- Callbacks that synchronize state to parent (openChange, indexChange, selectedChange) +- These receive a plain value (boolean, number, string/array), NOT a DOM Event +- For each: `name`, `signature` [ENGINEER], `stateLifted`, `defaultBehavior` +- Distinguish from `events` (onEscape, onLoadError — these receive a DOM Event) + +**Dual output** [ENGINEER] +- When component renders both a custom widget AND a hidden native form element (e.g. listbox + select[hidden]) +- Look for: "form submission", "native select", "hidden input" in the contract +- `customElement`, `nativeElement`, `syncProp`, `purpose` +- Log as gap if not mentioned in contract + +**Floating positioner** [ENGINEER] +- For components with floating overlays (menus, tooltips, combobox) +- `implementation` (evo-expander | browser-anchor | none), `library`, `placement`, `strategy`, `triggerElement` +- Log as gap — engineer determines from implementation + +**Caller obligations** [CONTRACT] — CRITICAL for a11y compliance +- Derive from the contract's "Label mechanism", "Open/close rules", "Form context", "Widget type" sections +- Common categories: `form-context`, `label`, `heading-structure`, `focus-management`, `open-trigger`, `icon-treatment` +- For each: `category`, `description`, `wcagCriterion` [ENGINEER], `consequence` [ENGINEER] +- If the contract has a "Required label strings" section but no explicit caller obligations section, + derive obligations from the described requirements +- 🔴 Any unmet obligation = WCAG AA failure — these MUST surface as blocking gaps + +**Internal data protocol** [ENGINEER — always a gap] +- data-* attributes placed on children for internal coordination (e.g. data-value on listbox options) +- Log as gap — engineer determines from source audit + +**Keyboard interactions** [CONTRACT] +- Only non-native interactions (Space/Enter on a button do not need listing) +- Widget arrow-key navigation goes in `keyboardModel`, not here +- For each: `key`, `action`, `condition`, `emittedEvent` [ENGINEER] + +**Events** [CONTRACT/INFER] +- Custom DOM-event-like callbacks (onEscape, onLoadError, onAnimationEnd) +- These receive a DOM Event object or equivalent +- Do NOT put state-lifting callbacks here — those go in `stateLiftingCallbacks` +- For each: `name`, `signature` [ENGINEER], `trigger`, `condition` + +**BEM** [AUDIT for block/elements/modifiers — ENGINEER for alternateBlock] +- `block` ← `$AUDIT.skin.bemBlocks[0]` (the primary block name) +- `elements[]` ← `$AUDIT.skin.bemElements[]` (name only; description is a soft gap) +- `modifiers[]` ← `$AUDIT.skin.bemModifiers[]` (name only; description is a soft gap) +- `alternateBlock` / `alternateBlockCondition` — still `[ENGINEER]`; log as low confidence gap +- If audit snapshot absent: log block/elements/modifiers as medium confidence gaps + +**Design tokens** [SPEC when present, otherwise AUDIT] +- `tokens` (named map) ← `$SPEC.tokens` translated to CSS custom property names: + replace `.` with `-`, prepend `--` (e.g. `color.background.primary` → `--color-background-primary`) + Store as `{ "background": "--color-background-primary", "border": "--color-border-subtle", ... }` + This map is the primary input to the `evo-style-component` skill. +- `designTokens[]` (flat list) ← `$AUDIT.skin.cssCustomProperties[]` — keep this separately for + audit-sourced token enumeration; no gap when found in snapshot +- If spec absent and audit absent: log `tokens` as medium confidence gap + +**Token variants** [SPEC when present] +- `tokenVariants` (keyed by prop name → enum value → token map) ← `$SPEC.tokenVariants` + Translate each token value from dot-notation to CSS custom property name (same rule as above). + Store as a nested map: `{ "type": { "warning": { "background": "--color-background-warning", ... } } }` + This is the primary input for the static component skill when generating per-modifier SCSS rules. + Include a `note` string on any variant whose token raises a dark mode concern (e.g. a foreground + token that is globally adaptive and may flip to a light value in dark mode). +- If spec absent: omit entirely; no gap needed (the static skill will infer from manifest.bem.modifiers) + +**Behaviors** [CONTRACT hint → ENGINEER] +- Non-obvious algorithms (e.g. color hash, aspect ratio detection, animation-gated close) +- Standard behavior kinds to use (see manifest-schema.md for full list): + `animationGatedClose`, `typeaheadSearch`, `lightDismiss`, `collapseOnSelect`, + `reactiveVisibility`, `dualOutputSync`, `colorDerived` +- The contract describes the *behavior*; the engineer supplies the implementation detail + +**Dependencies** [AUDIT for name/type — INFER for usedWhen] +- `name` ← `$AUDIT.marko.subComponents[]` (all evo-* tags used in the template) +- `type` ← derive from name: `evo-icon-*` → `icon`; names in `$AUDIT.marko.internalImports[]` → `internal-tag`; others → `component` +- `usedWhen` ← default to `'always'`; log each as a soft gap (`confidence: medium, source: inferred`) for engineer to refine to conditional where needed +- If audit snapshot absent: log as medium confidence gaps + +**RTL** [AUDIT — never from contract] +- RTL is a Skin-layer invariant — the contract does not declare it +- `notes` ← if `$AUDIT.skin.rtlOverrides === true`: populate with "This component has RTL-specific overrides in skin SCSS" and log as `confidence: high, source: audit` +- If `$AUDIT.skin.rtlOverrides === false`: omit the `rtl` field entirely (no gap needed) +- If audit snapshot absent: log as medium confidence gap + +**Storybook** [ENGINEER — always a gap] +- `category` (Storybook title path), `stories[]` + +--- + +## Step 4 — Write manifest.json + +Write `src/routes/_index/components/$COMPONENT/manifest.json`. Use the full schema from +`references/manifest-schema.md`. Omit sections where no information is available +rather than writing empty arrays — except for `gaps`, which is always present. + +Key structural rules: + +| Source | Gap entry? | Confidence | Blocks Gate 2? | +|---|---|---|---| +| `[SPEC]` — explicit in spec file | No | high | No | +| `[CONTRACT]` — explicit | No | — | — | +| `[AUDIT]` — found in snapshot | No | high | No | +| `[INFER]` — inferred from context | Yes | medium | No | +| `[AUDIT]` — snapshot absent | Yes | medium | No | +| `[ENGINEER]` — requires interpretation | Yes | low | **Yes** | +| `figmaUrl: null` — contract-first workflow | No | — | **Never blocks** | + +- `a11yProps` — extract from contract's "Required label strings" section; if silent, add a 🔴 low-confidence gap +- `bem.block/elements/modifiers`, `designTokens`, `dependencies` — fill from audit snapshot; no gap when found +- `bem.alternateBlock` — always a gap, but mark as `"informational": true`. It does not block static or CSS generation; only framework layers need it and engineers fill it after inspecting the template. Setting `"informational": true` ensures the validator never exits 1 on this field. +- `figma.nodeId` — always a gap when only a fileKey is available (no full node URL). Mark as `"informational": true`. It does not affect code generation; it only enables Code Connect navigation. Engineers fill it after the Figma design is finalized. +- `storybook.stories` — always a gap, but mark as `"informational": true` so the validator does not treat it as blocking. Story names cannot be known before generation runs; this field is post-generation by design. +- `figmaUrl` — if the contract explicitly states null or "not yet available", treat as a valid value with NO gap entry. A contract written before the Figma design exists is a first-class workflow, not a missing field. Add a single informational note: "figmaUrl will be populated in the subsequent visual-layer contract update (PR 2)." +- Never produce an empty `gaps` array — even a fully contract-sourced manifest will have alternateBlock gaps + +--- + +## Step 5 — Write gap-report.json + +Write `src/routes/_index/components/$COMPONENT/gap-report.json`: + +```json +{ + "component": "", + "generatedAt": "", + "summary": { + "totalGaps": 0, + "highConfidence": 0, + "mediumConfidence": 0, + "lowConfidence": 0, + "missingFields": 0 + }, + "requiresEngineerAction": [ + "" + ], + "gaps": [], + "deviations": [] +} +``` + +The `gaps` array must be identical to `manifest.json["gaps"]`. +`requiresEngineerAction` contains only `low` confidence or `missing` source gaps. +Medium-confidence gaps go in `gaps` only. + +**`deviations[]`** — records intentional departures from the spec's stated values. +When an implementation must differ from a spec-specified value (e.g. for WCAG compliance, +dark mode correctness, or engineering constraints), record the deviation here rather than +silently implementing a different value: + +```json +{ + "field": "tokenVariants.type.warning.iconColor", + "specValue": "color.foreground.primary", + "implementedValue": "color.foreground.on-warning", + "reason": "--color-foreground-primary resolves to #f7f7f7 in dark mode (1.6:1 contrast on yellow, fails 3:1 WCAG AA minimum for non-text). --color-foreground-on-warning is always neutral-800 (#191919).", + "wcagCriterion": "1.4.11", + "designReviewNeeded": true +} +``` + +Each deviation entry: +- `field` — the manifest/spec field path +- `specValue` — exact value the spec specified +- `implementedValue` — what was actually implemented +- `reason` — why the deviation was necessary (be specific — cite contrast ratios, token resolution behavior, etc.) +- `wcagCriterion` — WCAG criterion that motivated the change (omit if not WCAG-driven) +- `designReviewNeeded` — `true` if the spec must be updated to reflect this, `false` if it is a local implementation detail + +Deviations surface in the Gate 2 review so the engineer can decide whether to: +1. Confirm the deviation and flag it for design team follow-up +2. Revert to the spec value and solve the underlying cause differently + +--- + +## Step 6 — Validate + +Check if `scripts/codegen/validate-manifest.ts` exists. + +- **If yes:** Run `npx tsx scripts/codegen/validate-manifest.ts $COMPONENT`. + Report every validation error. Mark the manifest as `INVALID` if any errors + are found — do not suppress them or proceed. +- **If no:** Skip silently. Do not print anything about this step. + +--- + +## Step 7 — Surface the report and stop + +Print the following summary and then **stop completely**: + +``` +## Manifest generated: $COMPONENT + +Files written: + ✅ src/routes/_index/components/$COMPONENT/manifest.json + ✅ src/routes/_index/components/$COMPONENT/gap-report.json + +Gap summary: + 🔴 Requires engineer action (low confidence / missing): N + 🟡 Needs verification (inferred from context): N + 🟢 Confirmed from contract: N + +[If 🔴 gaps exist — list each with field path and what the engineer must provide] +[If 🟡 gaps exist — list each with what was assumed and why] + +[If deviations[] is non-empty:] +⚠️ Spec deviations (N) — implementation differs from spec-stated values: + • : spec= → implemented= + Reason: + Design review needed: [yes/no] + +───────────────────────────────────────────────── +GATE 2: Awaiting engineer approval. + +Review src/routes/_index/components/$COMPONENT/manifest.json and src/routes/_index/components/$COMPONENT/gap-report.json. +Resolve any 🔴 items. Verify 🟡 items. Confirm or escalate any ⚠️ deviations. +When satisfied, run `/evo-component $COMPONENT` to begin code generation. +───────────────────────────────────────────────── +``` + +Do not write any more files. Do not invoke any skills. The engineer owns what +happens next. diff --git a/.claude/skills/evo-create-component-manifest/evals/evals.json b/.claude/skills/evo-create-component-manifest/evals/evals.json new file mode 100644 index 0000000000..fba4f60ede --- /dev/null +++ b/.claude/skills/evo-create-component-manifest/evals/evals.json @@ -0,0 +1,57 @@ +{ + "skill_name": "evo-create-component-manifest", + "evals": [ + { + "id": 1, + "prompt": "/evo-create-component-manifest ebay-avatar", + "expected_output": "Produces contracts/ebay-avatar/manifest.json and contracts/ebay-avatar/gap-report.json. The manifest should be fully populated with props, slots, states, variants, BEM, design tokens, and a11y. The gap report should have zero or minimal gaps (0-2 medium-confidence) since the contract is complete. Ends with a GATE 2 summary and stops — does not write any component code.", + "files": [], + "expectations": [ + "manifest.json is written to contracts/ebay-avatar/manifest.json", + "gap-report.json is written to contracts/ebay-avatar/gap-report.json", + "manifest.json contains all five props (size, imageUrl, initials, a11yText, fallback) with correct types", + "manifest.json contains all three variants (image, initials, icon) with trigger conditions", + "manifest.json contains both slots (default) and states (loading, error)", + "manifest.json contains the BEM block 'avatar' with the correct elements and modifiers", + "manifest.json contains at least 4 design tokens", + "gap-report.json has zero low-confidence gaps", + "The skill prints a GATE 2 message and does not generate any SCSS, React, or Marko code", + "No downstream skill (/evo-component or similar) is invoked" + ] + }, + { + "id": 2, + "prompt": "create manifest for ebay-badge", + "expected_output": "Produces contracts/ebay-badge/manifest.json and contracts/ebay-badge/gap-report.json. The contract is partial — no Figma URL, no slots section, no states section, no design tokens, no a11y section. The gap report should have multiple low-confidence (missing) entries for these omitted fields. The GATE 2 summary must call out the required engineer actions before the manifest can be approved.", + "files": [], + "expectations": [ + "manifest.json is written to contracts/ebay-badge/manifest.json", + "gap-report.json is written to contracts/ebay-badge/gap-report.json", + "manifest.json has figmaUrl set to null with a corresponding gap entry", + "manifest.json has designTokens as an empty array or null with a corresponding gap entry", + "gap-report.json has at least 3 gaps with confidence 'low' or source 'missing'", + "gap-report.json requiresEngineerAction is non-empty", + "The GATE 2 summary lists the 🔴 gaps that the engineer must resolve", + "The skill prints a GATE 2 message and does not generate any component code", + "The manifest still includes the 3 props (count, max, variant) that are clearly defined in the contract" + ] + }, + { + "id": 3, + "prompt": "generate manifest from contract for ebay-progress-bar", + "expected_output": "Produces contracts/ebay-progress-bar/manifest.json and contracts/ebay-progress-bar/gap-report.json. The contract describes two modes in prose (determinate/indeterminate) without using a formal 'variants' table. The manifest should extract these as variants with medium-confidence gaps noting the prose source. Figma URL is explicitly stated as pending — should be null with a low-confidence gap. Design tokens are described as TBD — should be null with a low-confidence gap.", + "files": [], + "expectations": [ + "manifest.json is written to contracts/ebay-progress-bar/manifest.json", + "gap-report.json is written to contracts/ebay-progress-bar/gap-report.json", + "manifest.json has figmaUrl set to null with a gap entry noting it is pending", + "manifest.json has designTokens as null or empty with a gap entry noting tokens are TBD", + "manifest.json contains two variants (determinate and indeterminate) extracted from the prose description", + "The variant gap entries have confidence 'medium' and source 'inferred' (not 'missing')", + "The skill correctly uses medium confidence for inferred fields, not low confidence", + "gap-report.json requiresEngineerAction contains figmaUrl and designTokens", + "The skill prints a GATE 2 message and does not generate any component code" + ] + } + ] +} diff --git a/.claude/skills/evo-create-component-manifest/references/audit-plan.md b/.claude/skills/evo-create-component-manifest/references/audit-plan.md new file mode 100644 index 0000000000..53a846089d --- /dev/null +++ b/.claude/skills/evo-create-component-manifest/references/audit-plan.md @@ -0,0 +1,124 @@ +# Comprehensive Component Audit Plan + +## Context + +The manifest skill (`/evo-create-component-manifest`) was built after auditing only +two components: `evo-button` and `evo-avatar`. While those were enough to establish +the schema structure, they are both simple components. The goal of this audit is to +ensure the schema and contract template additions cover every pattern that exists +across the full evo-web component library — including complex keyboard navigation, +ARIA relationships, compound components, focus management, and MakeupJS delegation. + +## Current state (before this audit) + +- `manifest-schema.md` — schema derived from button + avatar audit; likely missing + fields for ARIA relationships, focus management, compound components, multi-event + patterns, and MakeupJS delegation +- `contract-template-additions.md` — describes what to add to the eBay design + contract format (NOT a separate structure); targets Accessibility Contract and + Platform Realizations › Web sections +- `SKILL.md` — updated to extract all fields from current schema + +## Goal + +Update `manifest-schema.md`, `contract-template-additions.md`, and `SKILL.md` to +reflect the full range of patterns found across all evo-web components. + +--- + +## Phase 1 — Static analysis script + +Write a script that scans all three package layers and produces a structured JSON +summary per component. Save output to +`.claude/skills/evo-create-component-manifest/scripts/audit-output/`. + +**From `packages/evo-marko/src/tags/`:** +- All Input interface props (name, type, required/optional) +- All `a11y*` and `aria-*` props +- All `on-*` event callbacks +- All AttrTag slot definitions (`<@name>`) +- All `aria-*` attribute usages directly in templates +- All MakeupJS imports (`@ebay/makeup-*`) +- All sub-component and icon imports + +**From `packages/skin/src/`:** +- All BEM blocks, elements, and modifiers +- All CSS custom properties consumed (`var(--*)`) +- All state selectors (`[disabled]`, `[aria-*]`, `[hidden]`, etc.) +- All RTL overrides (`[dir="rtl"]`) + +**From `packages/evo-react/src/`:** +- TypeScript prop interface definitions +- `aria-*` attribute usages +- Event handler type signatures + +Output: one JSON file per component + a cross-component summary of every unique +pattern found. + +--- + +## Phase 2 — Component categorization + +Group components from the script output by pattern type: + +| Category | Why it matters | +|---|---| +| Simple presentational | Baseline — already covered (avatar) | +| Simple interactive | Baseline — already covered (button) | +| Form inputs | `aria-invalid`, `aria-errormessage`, `aria-required`, fieldset/legend | +| Navigation | Tabs, breadcrumb — roving tabindex, `aria-selected` | +| Overlays | Dialog, tooltip, drawer — focus trap, `aria-modal`, `aria-controls` | +| Complex interactive | Combobox, menu, carousel — full keyboard models, ARIA relationships | +| Compound/parent-child | Components that orchestrate child components | + +--- + +## Phase 3 — Deep manual audit of representative complex components + +Pick one component from each uncovered category and read all three layers + docs. +Target candidates: + +- **Combobox** — complex keyboard model, ARIA relationships (`aria-controls`, + `aria-activedescendant`), MakeupJS delegation +- **Dialog** — focus trap, `aria-modal`, `aria-labelledby`, lifecycle events +- **Tabs** — compound pattern, `aria-selected`, roving tabindex, parent-child + coordination + +For each: skin SCSS → evo-marko → evo-react → website docs page. + +--- + +## Phase 4 — Website docs mining + +Scan `/src/routes/_index/components/` and `/src/routes/_index/accessibility/` for: +- Plain-English a11y guidance not reflected in source code +- Props documented with behavioral context +- RTL notes +- Known limitations or caveats + +Phases 3 and 4 can run in parallel once Phase 2 is complete. + +--- + +## Phase 5 — Schema gap analysis and updates + +1. Compare all discovered patterns against current `manifest-schema.md` +2. Identify fields the schema cannot represent +3. Update `manifest-schema.md` with new fields and real component examples +4. Update `contract-template-additions.md` if new contract-level information is needed +5. Update `SKILL.md` to extract any newly discovered field patterns + +--- + +## Important notes + +- The real eBay design contract format is a rich design-team document (Core + Invariants, Flex Zones, Interaction & Lifecycle, Platform Realizations, etc.). + `contract-template-additions.md` describes additions that fit *within* that + existing structure — NOT a replacement format. +- Not all information will be on the static layer. Some patterns only appear at + runtime (MakeupJS behavior, focus management). The manual audit (Phase 3) is + needed to catch these. +- The script should be saved to + `.claude/skills/evo-create-component-manifest/scripts/` for future re-runs as + new components are added. diff --git a/.claude/skills/evo-create-component-manifest/references/contract-template-additions.md b/.claude/skills/evo-create-component-manifest/references/contract-template-additions.md new file mode 100644 index 0000000000..b88b62d8ef --- /dev/null +++ b/.claude/skills/evo-create-component-manifest/references/contract-template-additions.md @@ -0,0 +1,394 @@ +# Contract Template Additions + +The real design contract format (as used at eBay) is a rich, design-team-oriented +document with sections like Core Invariants, Flex Zones, Interaction & Lifecycle, +Platform Realizations, and an Accessibility Contract. It is intentionally +implementation-agnostic. + +The manifest skill needs specific pieces of information that are typically absent from +contracts written in this format. Each addition below maps to the section of the real +contract where it naturally belongs — no new top-level sections are needed. + +--- + +## 0. Figma URL → Component header + +The contract may be written and merged before the Figma design exists. This is a +first-class workflow — the behavioral spec is independent of the visual design. + +When writing the contract before Figma exists, explicitly state the absence rather +than omitting the field. This tells the manifest skill the null is intentional, not +a missing piece: + +```markdown +**Figma URL**: Not yet available — will be added in the visual-layer contract update. +``` + +When the Figma design is ready, a second contract update (PR 2) adds the link: + +```markdown +**Figma URL**: https://figma.com/design/... +``` + +Merging PR 2 (after engineer review of the Figma design) triggers the visual-layer +build phase. The engineer's PR approval is the gate — no separate manifest review +step is needed for the visual layer. + +--- + +## 1. Required label strings → Accessibility Contract + +The Accessibility Contract describes what assistive technologies must be able to do. +It should also name the specific overridable text strings consumers must supply for +localization. + +Without this, the manifest skill cannot populate `a11yProps` and must flag every +i18n-able string as a 🔴 gap. + +**Add a "Required label strings" subsection inside Accessibility Contract:** + +```markdown +### Required label strings + +These strings are passed as props and must be overridable for localization. + +- `a11yText` (required, allowNull) — default: none — The accessible label for + the component. Typically "Signed in as [name]" or "Signed out". Pass null + only if a parent element already provides an accessible label for this + component. +- `a11yLoadingText` (optional) — default: "loading" — Announced when the + component enters a loading state. +``` + +Include one entry per string. State the prop name, whether it is required, +its default English value, and when it is announced or used. + +--- + +## 2. Explicit screen reader announcement → Accessibility Contract + +The Accessibility Contract often states what assistive technologies *must be +able to do* but not what they *actually say*. The manifest skill needs the latter +to populate `a11y.screenReaderAnnouncement`. + +**Add a "Screen reader announcement" subsection inside Accessibility Contract:** + +```markdown +### Screen reader announcement + +When encountered, a screen reader announces the value of `a11yText` — typically +"Signed in as [name]" or "Signed out". This component is purely presentational; +it is not announced as a button or interactive element. +``` + +For components with state changes: + +```markdown +### Screen reader announcement + +The button label is announced normally. When in loading state, the label is +replaced by the value of `a11yLoadingText` (default: "loading"). This change +is announced reactively — the screen reader does not need to refocus. +``` + +Plain English is sufficient. The engineer translates this to ARIA. + +--- + +## 3. Keyboard focusability → Accessibility Contract + +Many contracts address interactive vs. non-interactive in general terms but do +not explicitly state whether the component is keyboard focusable by default, +or what happens to focus when disabled. + +**Add a "Keyboard focusability" subsection inside Accessibility Contract:** + +For a non-interactive component: +```markdown +### Keyboard focusability + +Not keyboard focusable on its own. If placed inside a `