From 5470ecdbe2c41a55b5bc1ff6ec7f55c6bfe9eac1 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:27:36 -0400 Subject: [PATCH 01/26] docs(renderers): design for pluggable multi-framework renderers Add the approved design for abstracting code generation into pluggable renderer manifests (nextjs, vite, astro, sveltekit, expo), a renderer registry, registry-driven detection/dispatch, and a net-new Astro hybrid (.astro + React islands) converter. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...-05-28-multi-framework-renderers-design.md | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 docs/plans/2026-05-28-multi-framework-renderers-design.md diff --git a/docs/plans/2026-05-28-multi-framework-renderers-design.md b/docs/plans/2026-05-28-multi-framework-renderers-design.md new file mode 100644 index 0000000..6c50bcf --- /dev/null +++ b/docs/plans/2026-05-28-multi-framework-renderers-design.md @@ -0,0 +1,198 @@ +# Multi-Framework Output via Pluggable Renderers + +**Date:** 2026-05-28 +**Branch:** `18-multi-framework-output-support-nextjs-vite-astro` +**Status:** Design approved + +## Goal + +Support Next.js, Vite, and Astro output targets by abstracting code generation +into pluggable renderers and adding framework selection configuration. + +## Background + +The pipeline already separates two axes: + +- `outputTarget` — the component *language*: `react`, `vue`, `svelte`, `react-native` +- `framework.type` — the meta-framework: `nextjs-app`, `vite`, `remix`, `sveltekit`, `expo` + +Next.js and Vite already exist today, but only as React *sub-templates* +(`framework.type`), with framework knowledge duplicated across intake skills, +token config, TDD scaffolding, Phase-4 dispatch, and orchestration. Astro is +genuinely new and is special: it is a meta-framework that hosts interactive +"islands" of React/Vue/Svelte alongside zero-JS static components. + +## Decisions + +1. **Renderer axis.** Keep `outputTarget` as the language. Introduce a + first-class, pluggable `renderer` that owns scaffolding and emission. +2. **Declarative form.** Each renderer is a manifest (`renderer.json`) resolved + by a registry, mirroring the existing agent-plugin / `pipeline.config.json` + patterns. Consumers read the manifest instead of hardcoding framework logic. +3. **Registry-first, all frameworks.** Author manifests for all five + frameworks (`nextjs`, `vite`, `astro`, `sveltekit`, `expo`) so the registry + is the single source of truth for detection, dispatch, and config. Existing + converters keep working, resolved via the registry. Only Astro requires a + net-new template and converter. +4. **Astro = hybrid.** Static/presentational components are zero-JS `.astro` + files; interactive components are React islands (`.tsx`) with `client:*` + directives. Pairs with `outputTarget: react`. + +## Architecture + +### 1. Renderer manifest + +New directory: `renderers//renderer.json`, one per framework. The +`vue`/`svelte`/`react-native` languages are reached *through* their renderers +(e.g. `sveltekit` → language `svelte`). + +```jsonc +{ + "name": "nextjs", + "language": "react", // outputTarget family + "detect": { + "configFiles": ["next.config.*"], + "dependencies": ["next"], + "priority": 20 // higher wins on ambiguous projects + }, + "template": "templates/nextjs", + "component": { + "ext": ".tsx", + "dir": "src/components", + "pageRouting": "app-router" // app-router | pages | file-based | screens + }, + "test": { "runner": "vitest", "library": "@testing-library/react", "setup": "..." }, + "converter": "figma-react-converter", + "commands": { "dev": "pnpm dev", "build": "pnpm build", "test": "pnpm vitest run" }, + "phases": { "exclude": [] }, // e.g. expo excludes visual-diff/cross-browser + "capabilities": { "islands": false, "ssr": true, "darkMode": true } +} +``` + +Schema: `renderers/renderer.schema.json`. + +Registry: `scripts/renderer-registry.js` with `list`, `resolve `, and +`detect [dir]`, mirroring `agent-registry.js`. Validation: +`scripts/validate-renderer.js`, wired into `verify-all` / `ci`. + +### 2. Detection & dispatch + +- **Detection** replaces the duplicated sniffing in `figma-intake`, + `canva-intake`, and `screenshot-intake` with one call: + `node scripts/renderer-registry.js detect [projectDir]`. It walks each + manifest's `detect` block (config-file globs + `package.json` deps) and + returns the matching `renderer` + its `language`, or `null`. + - Match → intake writes `renderer` and `outputTarget` (= `manifest.language`). + - No match → ask the user; choices generated from the registry (`list`). + Greenfield React defaults to `vite`. + - Precedence is data-driven via `detect.priority` (e.g. `nextjs` beats a bare + `vite.config.*`), making today's implicit ordering explicit. +- **build-spec.json**: add required `renderer` (enum from the registry). + `outputTarget` stays for back-compat, always derivable from + `renderer.language`; intake keeps both in sync. `framework.type` is + deprecated (folded into `renderer`); specs with only `outputTarget` resolve to + that language's default renderer. +- **Phase-4 dispatch**: the hardcoded `outputTarget → agent` tables in + `build-from-{figma,canva,screenshot}.md` become `resolve ` → + `manifest.converter`. `build-from-canva` (today hardwired to + `canva-react-converter`) becomes multi-framework for free. +- **Orchestration**: `parallel-orchestration` reads `manifest.phases.exclude` + instead of branching on `outputTarget === "react-native"`. + +### 3. Astro renderer (net-new) + +Manifest `renderers/astro/renderer.json`: `language: react`, +`detect.configFiles: ["astro.config.*"]`, `component.ext: ".astro"` with +`islandExt: ".tsx"`, `pageRouting: "file-based"`, `converter: "astro-converter"`, +`capabilities.islands: true`. + +New template `templates/astro/`: `astro.config.mjs` (`@astrojs/react` + +`@astrojs/tailwind`), `tsconfig.json`, `tailwind.config`, Vitest config using +the Astro **Container API** (`experimental_AstroContainer`), `package.json`. +Reuses `templates/shared/` for eslint/prettier. + +New agent `.claude/agents/astro-converter.md` — the only net-new converter. +Rule, driven by build-spec signals already present: + +- Component has `action`, interactive `category`, or `businessLogic` → emit a + **React island** (`.tsx`, reusing React converter patterns) referenced from + the page with the right `client:*` directive (`client:load` for + above-the-fold, `client:visible` for below-the-fold). +- Otherwise → emit a **static `.astro`** component (zero JS), props typed via + the frontmatter `interface Props`. +- Pages → `src/pages/*.astro` composing both. + +**TDD for Astro** (`tdd-from-figma` reads `manifest.test`): React islands → +Vitest + RTL (identical to existing React path); static `.astro` → Vitest + +Astro Container API; page-level interactivity → Playwright E2E (Phase 6). + +### 4. Remaining consumers + +- **Token config** (`canva-token-inference`, `design-token-lock`): read + `manifest.template` for the Tailwind config shape and `manifest.language` for + CSS-var vs NativeWind output. Astro reuses the React/Tailwind path. +- **TDD scaffolding** (`tdd-from-figma`): read `manifest.test` + (`runner`/`library`/`setup`/`containerApi`) instead of an `outputTarget` map. +- **Visual-diff / responsive / dark-mode / cross-browser**: gated by + `manifest.phases.exclude` and `manifest.capabilities`. Expo's manifest carries + the existing react-native skips, now declared once in data. +- **Scaffolding** (`setup-project.sh`): `--next`/`--vite` become + `--renderer ` copying `manifest.template` + `templates/shared/`; old + flags kept as aliases. +- **Commands**: dev/build/test read `manifest.commands` so Expo + (`expo start`, `jest`) is not special-cased. +- **pipeline.config.json**: `appTypes` stays (deployment shape is orthogonal); + add a note that phase inclusion is also filtered by the resolved renderer. + +## Testing + +- **Unit (Vitest) for `renderer-registry.js`:** `list` returns 5 renderers; + `resolve` parses a manifest and errors clearly on unknown names; `detect` + against fixture dirs maps configs → renderer (`astro.config.mjs`→astro, + `next.config.ts`→nextjs, expo `app.json`→expo, bare `vite.config.ts`→vite, + `svelte.config.js`→sveltekit, empty→null); precedence fixture with both + `next.config` and `vite.config` resolves to `nextjs`. +- **Schema validation (`validate-renderer.js`):** every shipped manifest + validates; `converter` names an existing agent; `template` points to a real + dir; `language` is one of the four. Wired into `verify-all` / `ci`. +- **Astro converter behavioral check:** `test-fixtures/astro-*.build-spec.json` + with one interactive + one static component, asserting island→`.tsx`+`client:*` + and static→`.astro`. +- **Doc-count guard:** adding `astro-converter` bumps agents 53→54; update all + count references in the same change to keep `check-doc-counts.sh` green. + +## File inventory + +**New:** +- `renderers/renderer.schema.json` +- `renderers/{nextjs,vite,astro,sveltekit,expo}/renderer.json` +- `templates/astro/` (config, tailwind, vitest+container, package.json) +- `.claude/agents/astro-converter.md` +- `scripts/renderer-registry.js`, `scripts/validate-renderer.js` +- `scripts/__tests__/renderer-registry.test.*` + detection fixtures +- `.claude/test-fixtures/astro-*.build-spec.json` +- `docs/multi-framework/renderers.md` + +**Modified:** +- 3 intake skills (detection + write `renderer` + registry-sourced choices) +- `build-from-{figma,canva,screenshot}.md` (registry-driven dispatch) +- `parallel-orchestration` (`manifest.phases.exclude`) +- `tdd-from-figma`, `canva-token-inference`, `design-token-lock` (read manifest) +- `setup-project.sh` (`--renderer`) +- `verify-all.sh` / `ci` (add `validate-renderer`) +- build-spec schema/examples (add `renderer`, deprecate `framework.type`) +- Docs + count bumps (`CLAUDE.md`, `README.md`, `docs/multi-framework/README.md`) + +## Rollout (incremental, each independently verifiable) + +1. Schema + registry + validation + tests (no behavior change). +2. Author all 5 manifests; wire `validate-renderer` into CI. +3. Migrate detection (intake skills) → registry. +4. Migrate dispatch + orchestration + token/TDD consumers. +5. Astro template + `astro-converter` + Astro fixtures/tests. +6. Docs + count sync. + +## Backward compatibility + +Specs with only `outputTarget` still resolve (→ that language's default +renderer). `framework.type` is honored as a hint during a deprecation window. From 3c6d02d2c9f738ba35a97e01cae635904f3abe71 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:30:56 -0400 Subject: [PATCH 02/26] docs(renderers): add bite-sized implementation plan Six-phase TDD plan: schema+registry+validator, author 5 manifests + CI wiring, migrate detection, migrate dispatch/orchestration/token/TDD consumers, net-new Astro template+converter, then docs/count sync. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-05-28-multi-framework-renderers.md | 404 ++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 docs/plans/2026-05-28-multi-framework-renderers.md diff --git a/docs/plans/2026-05-28-multi-framework-renderers.md b/docs/plans/2026-05-28-multi-framework-renderers.md new file mode 100644 index 0000000..f2822d9 --- /dev/null +++ b/docs/plans/2026-05-28-multi-framework-renderers.md @@ -0,0 +1,404 @@ +# Multi-Framework Renderers Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Abstract code generation into declarative, pluggable renderer manifests (nextjs, vite, astro, sveltekit, expo) resolved by a registry, migrate every framework-branching consumer to read the registry, and add a net-new Astro hybrid (`.astro` + React islands) converter. + +**Architecture:** A new `renderers//renderer.json` manifest per framework, validated by `renderers/renderer.schema.json`. `scripts/renderer-registry.js` (`list`/`resolve`/`detect`) and `scripts/validate-renderer.js` mirror the existing `agent-registry.js`/`validate-agent-plugin.js`. Intake skills, Phase-4 dispatch, orchestration, token config, and TDD scaffolding stop hardcoding framework logic and read the resolved manifest. `outputTarget` stays as the language family (derivable from `manifest.language`); `framework.type` is deprecated. + +**Tech Stack:** Node ESM CLI scripts, Vitest (config at `scripts/__tests__/vitest.config.js`), ajv 8 for JSON-Schema validation, Markdown agents/skills/commands, Astro + `@astrojs/react` + `@astrojs/tailwind`. + +**Conventions to follow (match existing scripts):** +- ESM `.js`, `#!/usr/bin/env node` shebang, `parseArgs`, `--json`, `--root`/`--renderers-root` override for tests, `emit(json, payload, human)` helper. +- Exit codes: `0` ok · `1` resolution/validation failure · `2` usage/IO error. +- Tests use `execFileSync("node", [SCRIPT, ...args, "--renderers-root", root])`, fixtures under `scripts/__tests__/fixtures/`. +- Run script tests: `pnpm vitest run --config scripts/__tests__/vitest.config.js scripts/__tests__/` (this is what `./scripts/run-tests.sh` drives; `fileParallelism:false`). + +--- + +## Phase 1 — Schema, registry, validation (no behavior change) + +### Task 1: Renderer manifest JSON Schema + +**Files:** +- Create: `renderers/renderer.schema.json` + +**Step 1: Write the schema.** A JSON Schema (draft 2020-12) describing the manifest. Required: `name`, `language`, `detect`, `template`, `component`, `test`, `converter`, `commands`. `language` enum: `["react","vue","svelte","react-native"]`. `detect` = object with `configFiles` (array of glob strings), `dependencies` (array of strings), optional `priority` (integer, default 0). `component` = object with `ext` (string), optional `islandExt`, `dir`, `pageRouting` enum `["app-router","pages","file-based","screens"]`. `test` = object with `runner`, `library`, optional `setup`, `containerApi` (boolean). `commands` = object with `dev`, `build`, `test`. Optional `phases` = `{ exclude: string[] }`, `capabilities` = object of booleans (`islands`, `ssr`, `darkMode`). `additionalProperties:false` at the top level. + +**Step 2: Validate the schema is well-formed.** +Run: `node -e "const Ajv=require('ajv/dist/2020').default;new Ajv().compile(require('./renderers/renderer.schema.json'));console.log('ok')"` +Expected: prints `ok`. + +**Step 3: Commit.** +```bash +git add renderers/renderer.schema.json +git commit -m "feat(renderers): add renderer manifest JSON schema" +``` + +--- + +### Task 2: `renderer-registry.js` — `list` + +**Files:** +- Create: `scripts/renderer-registry.js` +- Create: `scripts/__tests__/renderer-registry.test.js` +- Create fixtures: `scripts/__tests__/fixtures/renderers/{nextjs,vite}/renderer.json` (minimal valid manifests for tests, independent of the real shipped ones) + +**Step 1: Write the failing test.** +```js +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { execFileSync } from "child_process"; +import { mkdtempSync, mkdirSync, cpSync, rmSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SCRIPT = join(__dirname, "..", "renderer-registry.js"); +const FIX = join(__dirname, "fixtures", "renderers"); + +let root; +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "renderers-")); + cpSync(FIX, root, { recursive: true }); +}); +afterEach(() => rmSync(root, { recursive: true, force: true })); + +function run(args, cwd) { + try { + const stdout = execFileSync("node", [SCRIPT, ...args, "--renderers-root", root], { + encoding: "utf-8", timeout: 30000, cwd: cwd || root, + }); + return { stdout, exitCode: 0 }; + } catch (e) { + return { stdout: e.stdout || "", stderr: e.stderr || "", exitCode: e.status }; + } +} + +describe("renderer-registry list", () => { + it("lists all renderers found under the renderers root", () => { + const r = run(["list", "--json"]); + expect(r.exitCode).toBe(0); + expect(JSON.parse(r.stdout).renderers.map((x) => x.name).sort()) + .toEqual(["nextjs", "vite"]); + }); +}); +``` + +**Step 2: Run it, verify it fails.** +Run: `pnpm vitest run --config scripts/__tests__/vitest.config.js scripts/__tests__/renderer-registry.test.js` +Expected: FAIL (script missing / no output). + +**Step 3: Implement `list`.** Create `renderer-registry.js` with `parseArgs` supporting `list|resolve|detect`, `--json`, `--renderers-root ` (default `resolve(__dirname,"..","renderers")`). Add `loadCatalog(root)`: glob `*/renderer.json`, parse each, key by `name`. `list` emits `{ renderers: [{name, language}] }`. + +**Step 4: Run test, verify pass.** Same command. Expected: PASS. + +**Step 5: Commit.** +```bash +git add scripts/renderer-registry.js scripts/__tests__/renderer-registry.test.js scripts/__tests__/fixtures/renderers +git commit -m "feat(renderers): add renderer-registry list command" +``` + +--- + +### Task 3: `renderer-registry.js` — `resolve` + +**Files:** Modify `scripts/renderer-registry.js`; Modify `scripts/__tests__/renderer-registry.test.js`. + +**Step 1: Add failing tests.** +```js +describe("renderer-registry resolve", () => { + it("resolves a manifest by name", () => { + const r = run(["resolve", "nextjs", "--json"]); + expect(r.exitCode).toBe(0); + const m = JSON.parse(r.stdout); + expect(m.name).toBe("nextjs"); + expect(m.language).toBe("react"); + }); + it("exits 2 on unknown renderer with a clear error", () => { + const r = run(["resolve", "nope", "--json"]); + expect(r.exitCode).toBe(2); + expect(r.stdout + r.stderr).toMatch(/unknown renderer/i); + }); +}); +``` + +**Step 2: Run, verify fail.** (resolve not implemented) + +**Step 3: Implement `resolve`.** Look up `catalog[name]`; if missing, `emit({ok:false,error:"Unknown renderer \"\""})` + `exit(2)`. Else print the full manifest JSON; `exit(0)`. + +**Step 4: Run, verify pass.** + +**Step 5: Commit.** +```bash +git add scripts/renderer-registry.js scripts/__tests__/renderer-registry.test.js +git commit -m "feat(renderers): add renderer-registry resolve command" +``` + +--- + +### Task 4: `renderer-registry.js` — `detect` + precedence + +**Files:** Modify `scripts/renderer-registry.js`; Modify the test; add detection fixtures under `scripts/__tests__/fixtures/projects/` (`nextjs-proj/next.config.ts`, `vite-proj/vite.config.ts`, `ambiguous-proj/{next.config.ts,vite.config.ts}`, `empty-proj/.gitkeep`). + +**Step 1: Add failing tests.** `detect ` returns the matching renderer name + language, honoring `detect.priority`: +```js +describe("renderer-registry detect", () => { + const proj = (n) => join(__dirname, "fixtures", "projects", n); + it("detects nextjs from next.config", () => { + const r = run(["detect", proj("nextjs-proj"), "--json"]); + expect(JSON.parse(r.stdout).renderer).toBe("nextjs"); + }); + it("detects vite from vite.config", () => { + expect(JSON.parse(run(["detect", proj("vite-proj"), "--json"]).stdout).renderer).toBe("vite"); + }); + it("prefers higher detect.priority on ambiguous projects (nextjs over vite)", () => { + expect(JSON.parse(run(["detect", proj("ambiguous-proj"), "--json"]).stdout).renderer).toBe("nextjs"); + }); + it("returns null when nothing matches", () => { + expect(JSON.parse(run(["detect", proj("empty-proj"), "--json"]).stdout).renderer).toBeNull(); + }); +}); +``` +> Note: fixtures' `nextjs/renderer.json` must carry `detect.priority` higher than `vite`'s for the ambiguity test. Mirror this in the real manifests (Phase 2). + +**Step 2: Run, verify fail.** + +**Step 3: Implement `detect`.** For each renderer (sorted by `detect.priority` desc), check whether any `configFiles` glob matches a file in `projectDir` (use `fs.readdirSync` + simple glob: translate `*` → regex), OR any `dependencies` entry appears in the project's `package.json` deps/devDeps. First match wins. Emit `{ renderer, language }` or `{ renderer: null }`; always `exit(0)` (detection is a query, not a failure). + +**Step 4: Run, verify pass.** + +**Step 5: Commit.** +```bash +git add scripts/renderer-registry.js scripts/__tests__/renderer-registry.test.js scripts/__tests__/fixtures/projects +git commit -m "feat(renderers): add renderer-registry detect with priority precedence" +``` + +--- + +### Task 5: `validate-renderer.js` + +**Files:** +- Create: `scripts/validate-renderer.js` +- Create: `scripts/__tests__/validate-renderer.test.js` + +**Step 1: Write failing tests.** Validator supports `--dir ` and `--all [--renderers-root ]`, `--json`. Checks: (a) manifest validates against `renderer.schema.json` via ajv; (b) `name` equals the directory name; (c) `template` path exists (resolved against repo root, or `--root` override); (d) `converter` names an agent file `.claude/agents/.md` that exists; (e) `language` is one of the four. Exit `0` valid · `1` invalid · `2` usage. Tests: a good fixture passes; a fixture whose `converter` points at a missing agent fails with exit 1 and a message matching `/converter/i`; a fixture with a bad `language` fails. + +**Step 2: Run, verify fail.** + +**Step 3: Implement** mirroring `validate-agent-plugin.js` structure (ajv compile of the schema, then structural checks, collect `issues[]`, emit JSON or human, exit accordingly). + +**Step 4: Run, verify pass.** + +**Step 5: Commit.** +```bash +git add scripts/validate-renderer.js scripts/__tests__/validate-renderer.test.js +git commit -m "feat(renderers): add validate-renderer (schema + cross-reference checks)" +``` + +--- + +## Phase 2 — Author all five manifests + wire CI + +### Task 6: Author the five real manifests + +**Files:** Create `renderers/{nextjs,vite,astro,sveltekit,expo}/renderer.json`. + +Values (confirm `converter`/`template`/`commands` against the repo before writing): +- **nextjs**: `language:react`, `detect:{configFiles:["next.config.*"],dependencies:["next"],priority:20}`, `template:"templates/nextjs"`, `component:{ext:".tsx",dir:"src/components",pageRouting:"app-router"}`, `test:{runner:"vitest",library:"@testing-library/react"}`, `converter:"figma-react-converter"`, `commands:{dev:"pnpm dev",build:"pnpm build",test:"pnpm vitest run"}`, `capabilities:{islands:false,ssr:true,darkMode:true}`. +- **vite**: like nextjs but `detect:{configFiles:["vite.config.*"],dependencies:["vite"],priority:5}` (low, so nextjs/astro/sveltekit win), `pageRouting:"file-based"` (or `"pages"` per react-router convention), `converter:"figma-react-converter"`, `template:"templates/vite"`. +- **astro**: `language:react`, `detect:{configFiles:["astro.config.*"],dependencies:["astro"],priority:10}`, `template:"templates/astro"` (created in Phase 5), `component:{ext:".astro",islandExt:".tsx",dir:"src/components",pageRouting:"file-based"}`, `test:{runner:"vitest",library:"@testing-library/react",containerApi:true}`, `converter:"astro-converter"` (created in Phase 5), `capabilities:{islands:true,ssr:true,darkMode:true}`. +- **sveltekit**: `language:svelte`, `detect:{configFiles:["svelte.config.*"],dependencies:["@sveltejs/kit"],priority:15}`, `template:"templates/sveltekit"`, `component:{ext:".svelte",dir:"src/lib/components",pageRouting:"file-based"}`, `test:{runner:"vitest",library:"@testing-library/svelte"}`, `converter:"svelte-converter"`. +- **expo**: `language:react-native`, `detect:{configFiles:["app.json","app.config.*"],dependencies:["expo"],priority:25}`, `template:"templates/expo"`, `component:{ext:".tsx",dir:"src/components",pageRouting:"screens"}`, `test:{runner:"jest",library:"@testing-library/react-native"}`, `converter:"react-native-converter"`, `commands:{dev:"pnpm expo start",build:"pnpm expo export",test:"pnpm jest"}`, `phases:{exclude:["visual-diff","cross-browser","responsive","dark-mode"]}`, `capabilities:{islands:false,ssr:false,darkMode:false}`. + +> The astro manifest references `templates/astro` and `astro-converter`, which don't exist until Phase 5. To keep `validate-renderer --all` green until then, **author the astro manifest in Phase 5** (Task 14), not here. Here, author only nextjs/vite/sveltekit/expo. + +**Step 1:** Write the four manifests (nextjs, vite, sveltekit, expo). +**Step 2: Validate them all.** +Run: `node scripts/validate-renderer.js --all --json` +Expected: exit 0, all valid. +**Step 3: Commit.** +```bash +git add renderers/nextjs renderers/vite renderers/sveltekit renderers/expo +git commit -m "feat(renderers): author nextjs/vite/sveltekit/expo manifests" +``` + +### Task 7: Wire validation into verify-all / ci + +**Files:** +- Create: `scripts/verify-renderers.sh` (thin wrapper: `exec node "$(dirname "$0")/validate-renderer.js" --all "$@"`, defensive skeleton matching `verify-agent-plugins.sh`). +- Modify: `scripts/verify-all.sh:62-72` — add `"renderers|./scripts/verify-renderers.sh|"` to `ALL_CHECKS`. + +**Step 1:** Add the wrapper + ALL_CHECKS entry. +**Step 2: Verify it runs.** +Run: `./scripts/verify-all.sh --include renderers` +Expected: `renderers … passed`. +**Step 3: Commit.** +```bash +git add scripts/verify-renderers.sh scripts/verify-all.sh +git commit -m "ci(renderers): wire validate-renderer into verify-all" +``` + +--- + +## Phase 3 — Migrate detection (intake skills) + +### Task 8: Replace duplicated detection with the registry + +**Files:** Modify `.claude/skills/figma-intake/SKILL.md`, `.claude/skills/canva-intake/SKILL.md`, `.claude/skills/screenshot-intake/SKILL.md`. + +For each, in the framework-detection section: +1. Replace the hand-written config-file/dep sniffing with: run `node scripts/renderer-registry.js detect . --json`. If `renderer` is non-null, set `renderer` and `outputTarget` (= the resolved `language`) in build-spec. +2. Replace the hardcoded "ask the user" framework list with: run `node scripts/renderer-registry.js list --json` and present those names as choices; greenfield React default = `vite`. +3. Document writing the new `renderer` field into build-spec (see Task 9). + +These are instruction-doc edits; verify by re-reading that each skill now references the registry commands and no longer enumerates `next.config.*`/`svelte.config.*` by hand. + +**Commit:** +```bash +git add .claude/skills/figma-intake .claude/skills/canva-intake .claude/skills/screenshot-intake +git commit -m "refactor(intake): detect framework via renderer-registry, not hardcoded sniffing" +``` + +### Task 9: build-spec schema/examples — add `renderer`, deprecate `framework.type` + +**Files:** Modify the build-spec definition referenced in the intake skills + any `.claude/test-fixtures/*.build-spec.json` examples + `docs/multi-framework/README.md`. + +- Add required `renderer` field (string; valid values = registry names). +- Keep `outputTarget`; document it as derived from `renderer.language`. +- Mark `framework.type` deprecated; back-compat rule: a spec with only `outputTarget` resolves to that language's **default renderer** (define defaults: react→vite, svelte→sveltekit, react-native→expo, vue→… leave as-is until a vue renderer exists). +- Update the example fixtures to include `renderer`. + +**Commit:** +```bash +git add .claude/test-fixtures docs/multi-framework/README.md +git commit -m "feat(build-spec): add renderer field, deprecate framework.type" +``` + +--- + +## Phase 4 — Migrate dispatch, orchestration, token, TDD consumers + +### Task 10: Registry-driven Phase-4 dispatch + +**Files:** Modify `.claude/commands/build-from-figma.md`, `.claude/commands/build-from-canva.md`, `.claude/commands/build-from-screenshot.md`. + +Replace each hardcoded `outputTarget → agent` table (and `build-from-canva`'s hardwired `canva-react-converter`) with: read `renderer` from build-spec → `node scripts/renderer-registry.js resolve --json` → dispatch `manifest.converter`. Keep a note that `canva`/`figma` sources may prefer the `*-react-converter` variants when `language===react`; encode that preference in the manifest `converter` field if needed (e.g. a `source` override map) — otherwise keep a single converter per renderer. + +**Commit:** `refactor(pipeline): dispatch Phase-4 converter via renderer manifest` + +### Task 11: Orchestration phase exclusion from manifest + +**Files:** Modify `.claude/skills/parallel-orchestration/SKILL.md`. + +Replace `outputTarget === "react-native"` phase-skipping with: resolve the renderer, read `manifest.phases.exclude`. Document that excluded phases are dropped from the dependency graph. + +**Commit:** `refactor(orchestration): exclude phases via manifest.phases.exclude` + +### Task 12: Token config reads the manifest + +**Files:** Modify `.claude/skills/canva-token-inference/SKILL.md`, `.claude/skills/design-token-lock/SKILL.md`. + +Replace `outputTarget`-branching for Tailwind/NativeWind config with: read `manifest.template` (Tailwind config shape) and `manifest.language` (CSS-var vs NativeWind). Note Astro reuses the React/Tailwind path. + +**Commit:** `refactor(tokens): derive token config target from renderer manifest` + +### Task 13: TDD scaffolding reads the manifest + +**Files:** Modify `.claude/skills/tdd-from-figma/SKILL.md`. + +Replace the `outputTarget → test runner` map with `manifest.test` (`runner`/`library`/`setup`/`containerApi`). Document the Astro split: islands → Vitest+RTL, `.astro` → Vitest + Container API, page interactivity → Playwright E2E. + +**Commit:** `refactor(tdd): select test runner/library from manifest.test` + +--- + +## Phase 5 — Astro template + converter (net-new) + +### Task 14: Astro template + +**Files:** Create `templates/astro/`: `astro.config.mjs` (integrations: `@astrojs/react`, `@astrojs/tailwind`), `tsconfig.json`, `tailwind.config.mjs`, `package.json` (astro + react + integrations + vitest + `@testing-library/react`), `vitest.config.ts` wiring the Astro **Container API** (`experimental_AstroContainer`), plus an example `src/pages/index.astro`. Reuse `templates/shared/` for eslint/prettier. + +**Verify:** the template's `package.json` lists pinned, current versions (check latest before pinning, per repo policy of keeping deps up to date). + +**Commit:** `feat(templates): add astro starter (react islands + tailwind + container-api tests)` + +### Task 15: Astro manifest + +**Files:** Create `renderers/astro/renderer.json` (values from Task 6's astro spec). + +**Verify:** `node scripts/validate-renderer.js --dir renderers/astro --json` → exit 0 (now that `templates/astro` and `astro-converter` exist; do this after Task 16). + +**Commit:** `feat(renderers): author astro manifest` + +### Task 16: `astro-converter` agent + +**Files:** Create `.claude/agents/astro-converter.md`. + +Model the frontmatter/structure on `figma-react-converter.md` (tools, "When to Use", autonomous workflow). Core documented rule: +- Read build-spec + locked tokens + screenshots. +- For each component: if it has `action`, an interactive `category`, or `businessLogic` → emit a **React island** `.tsx` (reuse React converter component/prop/test patterns) and reference it in the page with a `client:*` directive (`client:load` above-the-fold, `client:visible` below-the-fold). +- Else → emit a **static `.astro`** file, props typed via frontmatter `interface Props`, zero JS. +- Pages → `src/pages/*.astro` composing both. +- Tests: islands → Vitest+RTL; `.astro` → Vitest + Container API. + +**Step — doc-count bump (same commit):** adding an agent makes 53→54. Update every count reference; let `check-doc-counts.sh` confirm. +Run: `node scripts/check-doc-counts.sh` (or `./scripts/check-doc-counts.sh`) +Expected: passes with 54 agents. + +**Commit:** `feat(agents): add astro-converter (hybrid .astro + react islands)` + +### Task 17: Astro converter behavioral fixture + +**Files:** Create `.claude/test-fixtures/astro-hybrid.build-spec.json` with one interactive component (has `action`) and one static component, `renderer:"astro"`, `outputTarget:"react"`. If converter agents have spec-tests today (see `scripts/__tests__/canva-pipeline.test.js`), add an analogous assertion that the spec resolves to `astro-converter` and that the interactive/static split is well-formed; otherwise document the fixture as the contract reference. + +**Commit:** `test(renderers): add astro hybrid build-spec fixture` + +--- + +## Phase 6 — Scaffolding flag, docs, final sync + +### Task 18: `setup-project.sh --renderer` + +**Files:** Modify `scripts/setup-project.sh` (and its test if present). + +Add `--renderer `: resolve via registry, copy `manifest.template` + `templates/shared/`. Keep `--next`/`--vite` as aliases mapping to `--renderer nextjs`/`--renderer vite`. Update `--help`. + +**Verify:** `./scripts/setup-project.sh tmp-app --renderer astro --dry-run` (or equivalent dry path) names the astro template. + +**Commit:** `feat(setup): add --renderer flag backed by the registry` + +### Task 19: Renderer docs + +**Files:** Create `docs/multi-framework/renderers.md`; update `docs/multi-framework/README.md` and `CLAUDE.md` references. + +Cover: the three-axis model, manifest field reference, the registry commands, and a "How to add a renderer" checklist (author manifest → add template → set converter → `validate-renderer` → done). Add the new scripts to CLAUDE.md's script list. + +**Commit:** `docs(renderers): document the renderer model and how to add one` + +### Task 20: Full verification + count/architecture sync + +**Files:** `CLAUDE.md` (footer: agents 53→54, scripts count +2, "Last Updated" date), `README.md`, any onboarding docs referencing counts. + +**Step 1: Run the full gate.** +Run: `./scripts/verify-all.sh` +Expected: all checks pass, including `renderers` and `tests` (registry + validator specs) and the doc-count guard. +**Step 2: Run script tests explicitly.** +Run: `pnpm vitest run --config scripts/__tests__/vitest.config.js scripts/__tests__/renderer-registry.test.js scripts/__tests__/validate-renderer.test.js` +Expected: all pass. +**Step 3: Commit.** +```bash +git add -A +git commit -m "docs: sync counts and architecture for renderer system" +``` + +--- + +## Definition of done + +- `renderer-registry.js list/resolve/detect` and `validate-renderer.js` pass their Vitest specs. +- All five manifests validate; astro manifest references a real template + the `astro-converter` agent. +- Intake detection, Phase-4 dispatch, orchestration, token config, and TDD scaffolding all read the registry — no remaining hardcoded `outputTarget`/`framework.type` branches for these concerns (grep to confirm). +- `astro-converter` documents the island/static split; doc-count guard green at 54 agents. +- `./scripts/verify-all.sh` passes end to end. +- Back-compat: a build-spec with only `outputTarget` still resolves to a default renderer. From 66816c67e56d403586c25782f4b1487761001afd Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:34:26 -0400 Subject: [PATCH 03/26] feat(renderers): add renderer manifest JSON schema Co-Authored-By: Claude Opus 4.8 (1M context) --- renderers/renderer.schema.json | 115 +++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 renderers/renderer.schema.json diff --git a/renderers/renderer.schema.json b/renderers/renderer.schema.json new file mode 100644 index 0000000..d921f28 --- /dev/null +++ b/renderers/renderer.schema.json @@ -0,0 +1,115 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://aurelius.dev/renderer.schema.json", + "title": "Renderer Manifest", + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "language", + "detect", + "template", + "component", + "test", + "converter", + "commands" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "Unique renderer identifier; must match the directory basename" + }, + "language": { + "type": "string", + "enum": ["react", "vue", "svelte", "react-native"] + }, + "converter": { + "type": "string", + "minLength": 1, + "description": "Name of the converter agent (an agent file at .claude/agents/.md)" + }, + "detect": { + "type": "object", + "additionalProperties": false, + "required": ["configFiles", "dependencies"], + "properties": { + "configFiles": { + "type": "array", + "items": { "type": "string" }, + "description": "Glob strings matched against project files (basename)" + }, + "dependencies": { + "type": "array", + "items": { "type": "string" }, + "description": "Package names matched against the project's deps/devDeps" + }, + "priority": { + "type": "integer", + "default": 0, + "description": "Higher priority wins when multiple renderers match" + } + } + }, + "template": { + "type": "string", + "minLength": 1, + "description": "Path to the starter template directory" + }, + "component": { + "type": "object", + "additionalProperties": false, + "required": ["ext"], + "properties": { + "ext": { "type": "string", "minLength": 1 }, + "islandExt": { "type": "string" }, + "dir": { "type": "string" }, + "pageRouting": { + "type": "string", + "enum": ["app-router", "pages", "file-based", "screens"] + } + } + }, + "test": { + "type": "object", + "additionalProperties": false, + "required": ["runner", "library"], + "properties": { + "runner": { "type": "string", "minLength": 1 }, + "library": { "type": "string", "minLength": 1 }, + "setup": { "type": "string" }, + "containerApi": { "type": "boolean" } + } + }, + "commands": { + "type": "object", + "additionalProperties": false, + "required": ["dev", "build", "test"], + "properties": { + "dev": { "type": "string", "minLength": 1 }, + "build": { "type": "string", "minLength": 1 }, + "test": { "type": "string", "minLength": 1 } + } + }, + "phases": { + "type": "object", + "additionalProperties": false, + "required": ["exclude"], + "properties": { + "exclude": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "capabilities": { + "type": "object", + "additionalProperties": false, + "properties": { + "islands": { "type": "boolean" }, + "ssr": { "type": "boolean" }, + "darkMode": { "type": "boolean" } + } + } + } +} From 6f620697fe27de54e98e3c2b87a657196424d9b4 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:36:18 -0400 Subject: [PATCH 04/26] feat(renderers): add renderer-registry list command Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 2 + .../fixtures/renderers/nextjs/renderer.json | 25 +++ .../fixtures/renderers/vite/renderer.json | 24 +++ scripts/__tests__/renderer-registry.test.js | 33 +++ scripts/renderer-registry.js | 201 ++++++++++++++++++ 5 files changed, 285 insertions(+) create mode 100644 scripts/__tests__/fixtures/renderers/nextjs/renderer.json create mode 100644 scripts/__tests__/fixtures/renderers/vite/renderer.json create mode 100644 scripts/__tests__/renderer-registry.test.js create mode 100644 scripts/renderer-registry.js diff --git a/.gitignore b/.gitignore index 8de24ef..2cfae73 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,8 @@ playwright-report/ test-results/ scripts/__tests__/fixtures/* !scripts/__tests__/fixtures/agent-plugins/ +!scripts/__tests__/fixtures/renderers/ +!scripts/__tests__/fixtures/projects/ # Environment .env diff --git a/scripts/__tests__/fixtures/renderers/nextjs/renderer.json b/scripts/__tests__/fixtures/renderers/nextjs/renderer.json new file mode 100644 index 0000000..27a1808 --- /dev/null +++ b/scripts/__tests__/fixtures/renderers/nextjs/renderer.json @@ -0,0 +1,25 @@ +{ + "name": "nextjs", + "language": "react", + "converter": "figma-react-converter", + "detect": { + "configFiles": ["next.config.*"], + "dependencies": ["next"], + "priority": 10 + }, + "template": "templates/nextjs", + "component": { + "ext": ".tsx", + "dir": "components", + "pageRouting": "app-router" + }, + "test": { + "runner": "vitest", + "library": "@testing-library/react" + }, + "commands": { + "dev": "pnpm dev", + "build": "pnpm build", + "test": "pnpm vitest run" + } +} diff --git a/scripts/__tests__/fixtures/renderers/vite/renderer.json b/scripts/__tests__/fixtures/renderers/vite/renderer.json new file mode 100644 index 0000000..4bfa92c --- /dev/null +++ b/scripts/__tests__/fixtures/renderers/vite/renderer.json @@ -0,0 +1,24 @@ +{ + "name": "vite", + "language": "react", + "converter": "figma-react-converter", + "detect": { + "configFiles": ["vite.config.*"], + "dependencies": ["vite"], + "priority": 1 + }, + "template": "templates/vite", + "component": { + "ext": ".tsx", + "dir": "src/components" + }, + "test": { + "runner": "vitest", + "library": "@testing-library/react" + }, + "commands": { + "dev": "pnpm dev", + "build": "pnpm build", + "test": "pnpm vitest run" + } +} diff --git a/scripts/__tests__/renderer-registry.test.js b/scripts/__tests__/renderer-registry.test.js new file mode 100644 index 0000000..ecbd79d --- /dev/null +++ b/scripts/__tests__/renderer-registry.test.js @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { execFileSync } from "child_process"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SCRIPT = join(__dirname, "..", "renderer-registry.js"); +const RENDERERS_ROOT = join(__dirname, "fixtures", "renderers"); + +function run(args) { + try { + const stdout = execFileSync( + "node", + [SCRIPT, ...args, "--renderers-root", RENDERERS_ROOT], + { encoding: "utf-8", timeout: 30000 }, + ); + return { stdout, exitCode: 0 }; + } catch (e) { + return { stdout: e.stdout || "", stderr: e.stderr || "", exitCode: e.status }; + } +} + +describe("renderer-registry.js", () => { + it("lists available renderers sorted by name", () => { + const r = run(["list", "--json"]); + expect(r.exitCode).toBe(0); + expect( + JSON.parse(r.stdout) + .renderers.map((x) => x.name) + .sort(), + ).toEqual(["nextjs", "vite"]); + }); +}); diff --git a/scripts/renderer-registry.js b/scripts/renderer-registry.js new file mode 100644 index 0000000..c5c2acc --- /dev/null +++ b/scripts/renderer-registry.js @@ -0,0 +1,201 @@ +#!/usr/bin/env node +/** + * renderer-registry.js — List, resolve, and detect framework renderers. + * + * A renderer is a directory under the renderers root containing a + * renderer.json manifest (see renderers/renderer.schema.json). This CLI is a + * deterministic query surface over those manifests; it never mutates state. + * + * Usage: + * node scripts/renderer-registry.js list [--json] + * node scripts/renderer-registry.js resolve [--json] + * node scripts/renderer-registry.js detect [--json] + * (--renderers-root overrides the renderers root; used by tests) + * + * Exit codes: 0 ok · 1 resolution failure · 2 usage/IO error + */ +import { readFileSync, readdirSync, existsSync, statSync } from "fs"; +import { join, dirname, resolve, basename } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DEFAULT_RENDERERS_ROOT = resolve(__dirname, "..", "renderers"); + +function parseArgs(argv) { + const out = { cmd: null, arg: null, json: false, renderersRoot: DEFAULT_RENDERERS_ROOT }; + const positional = []; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--json") out.json = true; + else if (a === "--renderers-root") out.renderersRoot = resolve(argv[++i]); + else if (a === "-h" || a === "--help") { + printHelp(); + process.exit(0); + } else if (a.startsWith("--")) { + console.error(`Unknown argument: ${a}`); + printHelp(); + process.exit(2); + } else positional.push(a); + } + out.cmd = positional[0] ?? null; + out.arg = positional[1] ?? null; + return out; +} + +function printHelp() { + console.log(`Usage: node scripts/renderer-registry.js [arg] [options] + +Commands: + list List available renderers + resolve Print a renderer's full manifest + detect Detect which renderer matches a project + +Options: + --json Machine-readable output + --renderers-root Override the renderers root (default: ./renderers) + -h, --help Show this message`); +} + +/** Build a catalog { name -> { dir, manifest } } from renderer.json files + * under the renderers root. Skips unreadable or malformed manifests so one + * bad renderer can't abort the scan. */ +function loadCatalog(root) { + const catalog = {}; + if (!existsSync(root)) return catalog; + for (const entry of readdirSync(root)) { + const dir = join(root, entry); + let isDir; + try { + isDir = statSync(dir).isDirectory(); + } catch { + continue; + } + if (!isDir) continue; + const manifestPath = join(dir, "renderer.json"); + if (!existsSync(manifestPath)) continue; + let manifest; + try { + manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); + } catch { + continue; + } + if (!manifest || typeof manifest.name !== "string") continue; + catalog[manifest.name] = { dir, manifest }; + } + return catalog; +} + +/** Translate a simple glob (only `*` wildcards) to an anchored regex. */ +function globToRegExp(glob) { + const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*"); + return new RegExp(`^${escaped}$`); +} + +function emit(json, payload, human) { + if (json) console.log(JSON.stringify(payload, null, 2)); + else human(); +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + if (!args.cmd) { + printHelp(); + process.exit(2); + } + + const catalog = loadCatalog(args.renderersRoot); + + if (args.cmd === "list") { + const renderers = Object.values(catalog) + .map((c) => ({ name: c.manifest.name, language: c.manifest.language })) + .sort((a, b) => a.name.localeCompare(b.name)); + emit(args.json, { renderers }, () => { + if (!renderers.length) console.log("No renderers found."); + for (const r of renderers) console.log(`${r.name} (${r.language})`); + }); + process.exit(0); + } + + if (args.cmd === "resolve") { + if (!args.arg) { + console.error("This command requires a renderer name."); + process.exit(2); + } + const entry = catalog[args.arg]; + if (!entry) { + emit(args.json, { ok: false, error: `Unknown renderer "${args.arg}"` }, () => + console.error(`✗ Unknown renderer "${args.arg}"`), + ); + process.exit(2); + } + emit(args.json, entry.manifest, () => console.log(JSON.stringify(entry.manifest, null, 2))); + process.exit(0); + } + + if (args.cmd === "detect") { + if (!args.arg) { + console.error("This command requires a project directory."); + process.exit(2); + } + const projectDir = resolve(args.arg); + const match = detect(catalog, projectDir); + if (match) { + emit(args.json, { renderer: match.name, language: match.language }, () => + console.log(`${match.name} (${match.language})`), + ); + } else { + emit(args.json, { renderer: null }, () => console.log("No renderer detected.")); + } + process.exit(0); + } + + console.error(`Unknown command: ${args.cmd}`); + printHelp(); + process.exit(2); +} + +/** Find the first matching renderer, highest detect.priority first. A renderer + * matches if any configFiles glob matches a file basename in projectDir, or + * any dependencies entry appears in the project's package.json deps/devDeps. */ +function detect(catalog, projectDir) { + const ranked = Object.values(catalog).sort( + (a, b) => (b.manifest.detect?.priority ?? 0) - (a.manifest.detect?.priority ?? 0), + ); + + let files = []; + try { + files = readdirSync(projectDir); + } catch { + files = []; + } + + let pkgDeps = {}; + const pkgPath = join(projectDir, "package.json"); + if (existsSync(pkgPath)) { + try { + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); + pkgDeps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }; + } catch { + pkgDeps = {}; + } + } + + for (const { manifest } of ranked) { + const det = manifest.detect ?? {}; + const configFiles = det.configFiles ?? []; + const dependencies = det.dependencies ?? []; + + const fileMatch = configFiles.some((glob) => { + const re = globToRegExp(basename(glob)); + return files.some((f) => re.test(f)); + }); + const depMatch = dependencies.some((d) => d in pkgDeps); + + if (fileMatch || depMatch) { + return { name: manifest.name, language: manifest.language }; + } + } + return null; +} + +main(); From 67441d9cdf163c8d4b2deb5fdd9c9fe5df79cdd1 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:36:49 -0400 Subject: [PATCH 05/26] feat(renderers): add renderer-registry resolve command Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/__tests__/renderer-registry.test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/__tests__/renderer-registry.test.js b/scripts/__tests__/renderer-registry.test.js index ecbd79d..3e86f4b 100644 --- a/scripts/__tests__/renderer-registry.test.js +++ b/scripts/__tests__/renderer-registry.test.js @@ -30,4 +30,18 @@ describe("renderer-registry.js", () => { .sort(), ).toEqual(["nextjs", "vite"]); }); + + it("resolves a known renderer to its full manifest", () => { + const r = run(["resolve", "nextjs", "--json"]); + expect(r.exitCode).toBe(0); + const manifest = JSON.parse(r.stdout); + expect(manifest.name).toBe("nextjs"); + expect(manifest.language).toBe("react"); + }); + + it("exits 2 on an unknown renderer", () => { + const r = run(["resolve", "does-not-exist", "--json"]); + expect(r.exitCode).toBe(2); + expect(r.stdout).toMatch(/unknown renderer/i); + }); }); From 5bb6254d53cee0a46408444de1b57884bab36c41 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:37:40 -0400 Subject: [PATCH 06/26] feat(renderers): add renderer-registry detect with priority precedence Co-Authored-By: Claude Opus 4.8 (1M context) --- .../projects/ambiguous-proj/next.config.ts | 5 ++++ .../projects/ambiguous-proj/vite.config.ts | 3 +++ .../fixtures/projects/empty-proj/.gitkeep | 0 .../projects/nextjs-proj/next.config.ts | 5 ++++ .../projects/vite-proj/vite.config.ts | 3 +++ scripts/__tests__/renderer-registry.test.js | 25 +++++++++++++++++++ 6 files changed, 41 insertions(+) create mode 100644 scripts/__tests__/fixtures/projects/ambiguous-proj/next.config.ts create mode 100644 scripts/__tests__/fixtures/projects/ambiguous-proj/vite.config.ts create mode 100644 scripts/__tests__/fixtures/projects/empty-proj/.gitkeep create mode 100644 scripts/__tests__/fixtures/projects/nextjs-proj/next.config.ts create mode 100644 scripts/__tests__/fixtures/projects/vite-proj/vite.config.ts diff --git a/scripts/__tests__/fixtures/projects/ambiguous-proj/next.config.ts b/scripts/__tests__/fixtures/projects/ambiguous-proj/next.config.ts new file mode 100644 index 0000000..cb651cd --- /dev/null +++ b/scripts/__tests__/fixtures/projects/ambiguous-proj/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/scripts/__tests__/fixtures/projects/ambiguous-proj/vite.config.ts b/scripts/__tests__/fixtures/projects/ambiguous-proj/vite.config.ts new file mode 100644 index 0000000..6150f9a --- /dev/null +++ b/scripts/__tests__/fixtures/projects/ambiguous-proj/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "vite"; + +export default defineConfig({}); diff --git a/scripts/__tests__/fixtures/projects/empty-proj/.gitkeep b/scripts/__tests__/fixtures/projects/empty-proj/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/__tests__/fixtures/projects/nextjs-proj/next.config.ts b/scripts/__tests__/fixtures/projects/nextjs-proj/next.config.ts new file mode 100644 index 0000000..cb651cd --- /dev/null +++ b/scripts/__tests__/fixtures/projects/nextjs-proj/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/scripts/__tests__/fixtures/projects/vite-proj/vite.config.ts b/scripts/__tests__/fixtures/projects/vite-proj/vite.config.ts new file mode 100644 index 0000000..6150f9a --- /dev/null +++ b/scripts/__tests__/fixtures/projects/vite-proj/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "vite"; + +export default defineConfig({}); diff --git a/scripts/__tests__/renderer-registry.test.js b/scripts/__tests__/renderer-registry.test.js index 3e86f4b..13d0b78 100644 --- a/scripts/__tests__/renderer-registry.test.js +++ b/scripts/__tests__/renderer-registry.test.js @@ -6,6 +6,7 @@ import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const SCRIPT = join(__dirname, "..", "renderer-registry.js"); const RENDERERS_ROOT = join(__dirname, "fixtures", "renderers"); +const PROJ = (name) => join(__dirname, "fixtures", "projects", name); function run(args) { try { @@ -44,4 +45,28 @@ describe("renderer-registry.js", () => { expect(r.exitCode).toBe(2); expect(r.stdout).toMatch(/unknown renderer/i); }); + + it("detects nextjs from a next.config.ts project", () => { + const r = run(["detect", PROJ("nextjs-proj"), "--json"]); + expect(r.exitCode).toBe(0); + expect(JSON.parse(r.stdout).renderer).toBe("nextjs"); + }); + + it("detects vite from a vite.config.ts project", () => { + const r = run(["detect", PROJ("vite-proj"), "--json"]); + expect(r.exitCode).toBe(0); + expect(JSON.parse(r.stdout).renderer).toBe("vite"); + }); + + it("resolves an ambiguous project to the higher-priority renderer (nextjs)", () => { + const r = run(["detect", PROJ("ambiguous-proj"), "--json"]); + expect(r.exitCode).toBe(0); + expect(JSON.parse(r.stdout).renderer).toBe("nextjs"); + }); + + it("detects nothing in an empty project (renderer null, exit 0)", () => { + const r = run(["detect", PROJ("empty-proj"), "--json"]); + expect(r.exitCode).toBe(0); + expect(JSON.parse(r.stdout).renderer).toBeNull(); + }); }); From 4519a23d76edd839a58e5198517a4bdf559359e0 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:38:58 -0400 Subject: [PATCH 07/26] feat(renderers): add validate-renderer (schema + cross-reference checks) Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/__tests__/validate-renderer.test.js | 107 ++++++++++ scripts/validate-renderer.js | 213 ++++++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100644 scripts/__tests__/validate-renderer.test.js create mode 100644 scripts/validate-renderer.js diff --git a/scripts/__tests__/validate-renderer.test.js b/scripts/__tests__/validate-renderer.test.js new file mode 100644 index 0000000..43dd46a --- /dev/null +++ b/scripts/__tests__/validate-renderer.test.js @@ -0,0 +1,107 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { execFileSync } from "child_process"; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "fs"; +import { tmpdir } from "os"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SCRIPT = join(__dirname, "..", "validate-renderer.js"); + +let root, renderersRoot; + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "rend-validate-")); + // Fake repo root: an agents dir with one converter agent, and a template dir. + mkdirSync(join(root, ".claude", "agents"), { recursive: true }); + writeFileSync(join(root, ".claude", "agents", "figma-react-converter.md"), "# converter\n"); + mkdirSync(join(root, "templates", "nextjs"), { recursive: true }); + renderersRoot = join(root, "renderers"); + mkdirSync(renderersRoot, { recursive: true }); +}); + +afterEach(() => rmSync(root, { recursive: true, force: true })); + +function validManifest(overrides = {}) { + return { + name: "nextjs", + language: "react", + converter: "figma-react-converter", + detect: { configFiles: ["next.config.*"], dependencies: ["next"], priority: 10 }, + template: "templates/nextjs", + component: { ext: ".tsx", dir: "components", pageRouting: "app-router" }, + test: { runner: "vitest", library: "@testing-library/react" }, + commands: { dev: "pnpm dev", build: "pnpm build", test: "pnpm vitest run" }, + ...overrides, + }; +} + +function writeRenderer(name, manifest) { + const dir = join(renderersRoot, name); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, "renderer.json"), JSON.stringify(manifest, null, 2)); + return dir; +} + +function run(args) { + try { + const stdout = execFileSync("node", [SCRIPT, ...args, "--root", root], { + encoding: "utf-8", + timeout: 30000, + }); + return { stdout, exitCode: 0 }; + } catch (e) { + return { stdout: e.stdout || "", stderr: e.stderr || "", exitCode: e.status }; + } +} + +describe("validate-renderer.js", () => { + it("passes a well-formed renderer", () => { + const dir = writeRenderer("nextjs", validManifest()); + const r = run(["--dir", dir, "--json"]); + expect(r.exitCode).toBe(0); + expect(JSON.parse(r.stdout).ok).toBe(true); + }); + + it("fails when the converter points at a missing agent (exit 1)", () => { + const dir = writeRenderer("nextjs", validManifest({ converter: "no-such-agent" })); + const r = run(["--dir", dir, "--json"]); + expect(r.exitCode).toBe(1); + expect(JSON.parse(r.stdout).ok).toBe(false); + expect(r.stdout).toMatch(/converter/i); + }); + + it("fails when language is not one of the four (exit 1)", () => { + const dir = writeRenderer("nextjs", validManifest({ language: "angular" })); + const r = run(["--dir", dir, "--json"]); + expect(r.exitCode).toBe(1); + expect(JSON.parse(r.stdout).ok).toBe(false); + }); + + it("fails when manifest name does not match the directory basename (exit 1)", () => { + const dir = writeRenderer("nextjs", validManifest({ name: "wrong-name" })); + const r = run(["--dir", dir, "--json"]); + expect(r.exitCode).toBe(1); + expect(r.stdout).toMatch(/directory/i); + }); + + it("fails when the template path does not exist (exit 1)", () => { + const dir = writeRenderer("nextjs", validManifest({ template: "templates/does-not-exist" })); + const r = run(["--dir", dir, "--json"]); + expect(r.exitCode).toBe(1); + expect(r.stdout).toMatch(/template/i); + }); + + it("validates every renderer with --all", () => { + writeRenderer("nextjs", validManifest()); + writeRenderer("vite", validManifest({ name: "vite", template: "templates/nextjs" })); + const r = run(["--all", "--renderers-root", renderersRoot, "--json"]); + expect(r.exitCode).toBe(0); + expect(JSON.parse(r.stdout).count).toBe(2); + }); + + it("exits 2 with no --dir or --all", () => { + const r = run(["--json"]); + expect(r.exitCode).toBe(2); + }); +}); diff --git a/scripts/validate-renderer.js b/scripts/validate-renderer.js new file mode 100644 index 0000000..f069522 --- /dev/null +++ b/scripts/validate-renderer.js @@ -0,0 +1,213 @@ +#!/usr/bin/env node +/** + * validate-renderer.js — Validate a renderer manifest against the JSON Schema + * plus cross-reference checks the schema cannot express: + * - manifest validates against renderers/renderer.schema.json (ajv 2020) + * - manifest.name matches the directory basename + * - the template path exists (resolved against --root) + * - the converter names an existing agent at .claude/agents/.md + * - language is one of the four supported values + * + * Usage: + * node scripts/validate-renderer.js --dir [--json] + * node scripts/validate-renderer.js --all [--renderers-root ] [--json] + * (--root overrides the repo root for resolving template/agent paths) + * + * Exit codes: 0 valid · 1 invalid · 2 usage/IO error + */ +import { readFileSync, existsSync, statSync, readdirSync } from "fs"; +import { join, dirname, resolve, basename } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, ".."); +const SCHEMA = join(repoRoot, "renderers", "renderer.schema.json"); +const VALID_LANGUAGES = ["react", "vue", "svelte", "react-native"]; + +function parseArgs(argv) { + const out = { dir: null, all: false, json: false, renderersRoot: null, root: repoRoot }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--dir") out.dir = resolve(argv[++i]); + else if (a === "--all") out.all = true; + else if (a === "--renderers-root") out.renderersRoot = resolve(argv[++i]); + else if (a === "--root") out.root = resolve(argv[++i]); + else if (a === "--json") out.json = true; + else if (a === "-h" || a === "--help") { + printHelp(); + process.exit(0); + } else { + console.error(`Unknown argument: ${a}`); + printHelp(); + process.exit(2); + } + } + if (out.dir && out.all) { + console.error("Use either --dir or --all, not both."); + process.exit(2); + } + if (!out.renderersRoot) out.renderersRoot = join(out.root, "renderers"); + return out; +} + +function printHelp() { + console.log(`Usage: node scripts/validate-renderer.js (--dir | --all) [options] + +Options: + --dir Validate a single renderer directory + --all Validate every renderer under the renderers root + --renderers-root Renderers root for --all (default: /renderers) + --root Repo root for resolving template/agent paths + --json Machine-readable output + -h, --help Show this message`); +} + +async function compileSchema() { + const { default: Ajv2020 } = await import("ajv/dist/2020.js"); + const schema = JSON.parse(readFileSync(SCHEMA, "utf-8")); + return new Ajv2020({ allErrors: true, strict: false }).compile(schema); +} + +/** Flatten an ajv error to { path, message } with the offending key surfaced. */ +function formatSchemaError(err) { + const path = err.instancePath || "(root)"; + let message = err.message ?? "validation failed"; + if (err.params?.additionalProperty) message += `: "${err.params.additionalProperty}"`; + if (err.params?.missingProperty) message += `: "${err.params.missingProperty}"`; + if (err.params?.allowedValues) message += ` (allowed: ${err.params.allowedValues.join(", ")})`; + return { path, message }; +} + +function loadManifest(dir) { + const p = join(dir, "renderer.json"); + if (!existsSync(p)) throw new Error(`No renderer.json in ${dir}`); + return JSON.parse(readFileSync(p, "utf-8")); +} + +function validateRenderer(dir, validate, root) { + const issues = []; + const manifest = loadManifest(dir); // may throw -> caller reports it as an (io) issue + + if (!validate(manifest)) { + for (const e of validate.errors ?? []) issues.push(formatSchemaError(e)); + } + + const dirName = basename(dir); + if (manifest.name && manifest.name !== dirName) { + issues.push({ + path: "name", + message: `manifest name "${manifest.name}" != directory "${dirName}"`, + }); + } + + if (manifest.language && !VALID_LANGUAGES.includes(manifest.language)) { + issues.push({ + path: "language", + message: `invalid language "${manifest.language}" (allowed: ${VALID_LANGUAGES.join(", ")})`, + }); + } + + if (manifest.template) { + const templatePath = resolve(root, manifest.template); + if (!existsSync(templatePath)) { + issues.push({ path: "template", message: `template path not found: ${manifest.template}` }); + } + } + + if (manifest.converter) { + const agentFile = join(root, ".claude", "agents", `${manifest.converter}.md`); + if (!existsSync(agentFile)) { + issues.push({ + path: "converter", + message: `converter agent not found: .claude/agents/${manifest.converter}.md`, + }); + } + } + + return { name: manifest.name ?? dirName, dir, ok: issues.length === 0, issues }; +} + +function listRendererDirs(root) { + const dirs = []; + if (!existsSync(root)) return dirs; + for (const entry of readdirSync(root)) { + const full = join(root, entry); + try { + if (statSync(full).isDirectory() && existsSync(join(full, "renderer.json"))) dirs.push(full); + } catch { + /* skip unreadable entry */ + } + } + return dirs; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + if (!args.dir && !args.all) { + printHelp(); + process.exit(2); + } + + let validate; + try { + validate = await compileSchema(); + } catch (e) { + if (args.json) console.log(JSON.stringify({ ok: false, error: e.message })); + else console.error(`✗ ${e.message}`); + process.exit(2); + } + + let dirs; + if (args.all) { + dirs = listRendererDirs(args.renderersRoot); + } else { + if (!existsSync(join(args.dir, "renderer.json"))) { + const msg = `No renderer.json found in ${args.dir}`; + if (args.json) console.log(JSON.stringify({ ok: false, error: msg })); + else console.error(`✗ ${msg}`); + process.exit(2); + } + dirs = [args.dir]; + } + + const results = []; + for (const d of dirs) { + try { + results.push(validateRenderer(d, validate, args.root)); + } catch (e) { + results.push({ + name: basename(d), + dir: d, + ok: false, + issues: [{ path: "(io)", message: e.message }], + }); + } + } + + const ok = results.every((r) => r.ok); + if (args.json) { + console.log( + JSON.stringify( + { ok, count: results.length, results, issues: results.flatMap((r) => r.issues) }, + null, + 2, + ), + ); + } else { + if (args.all && results.length === 0) + console.log(`No renderers found under ${args.renderersRoot}`); + for (const r of results) { + if (r.ok) console.log(`✓ ${r.name}`); + else { + console.log(`✗ ${r.name} (${r.issues.length} issue(s)):`); + for (const i of r.issues) console.log(` ${i.path}: ${i.message}`); + } + } + } + process.exit(ok ? 0 : 1); +} + +main().catch((e) => { + console.error(`✗ Unhandled error: ${e.stack ?? e.message}`); + process.exit(2); +}); From b7d6160762cd90ada8fd69f5fcd0cb73005eb2a2 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:48:22 -0400 Subject: [PATCH 08/26] refactor(renderers): extract renderer-lib, guard trailing flags, stable detect tie-break Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/__tests__/renderer-registry.test.js | 67 ++++++++++++++++++-- scripts/__tests__/validate-renderer.test.js | 14 +++++ scripts/renderer-lib.js | 70 +++++++++++++++++++++ scripts/renderer-registry.js | 64 +++++++------------ scripts/validate-renderer.js | 41 +++++------- 5 files changed, 184 insertions(+), 72 deletions(-) create mode 100644 scripts/renderer-lib.js diff --git a/scripts/__tests__/renderer-registry.test.js b/scripts/__tests__/renderer-registry.test.js index 13d0b78..7f10f45 100644 --- a/scripts/__tests__/renderer-registry.test.js +++ b/scripts/__tests__/renderer-registry.test.js @@ -1,5 +1,7 @@ import { describe, it, expect } from "vitest"; import { execFileSync } from "child_process"; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "fs"; +import { tmpdir } from "os"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; @@ -10,11 +12,23 @@ const PROJ = (name) => join(__dirname, "fixtures", "projects", name); function run(args) { try { - const stdout = execFileSync( - "node", - [SCRIPT, ...args, "--renderers-root", RENDERERS_ROOT], - { encoding: "utf-8", timeout: 30000 }, - ); + const stdout = execFileSync("node", [SCRIPT, ...args, "--renderers-root", RENDERERS_ROOT], { + encoding: "utf-8", + timeout: 30000, + }); + return { stdout, exitCode: 0 }; + } catch (e) { + return { stdout: e.stdout || "", stderr: e.stderr || "", exitCode: e.status }; + } +} + +/** Run without auto-appending --renderers-root, for arg-parsing edge cases. */ +function runRaw(args) { + try { + const stdout = execFileSync("node", [SCRIPT, ...args], { + encoding: "utf-8", + timeout: 30000, + }); return { stdout, exitCode: 0 }; } catch (e) { return { stdout: e.stdout || "", stderr: e.stderr || "", exitCode: e.status }; @@ -69,4 +83,47 @@ describe("renderer-registry.js", () => { expect(r.exitCode).toBe(0); expect(JSON.parse(r.stdout).renderer).toBeNull(); }); + + it("exits 2 when --renderers-root is the last arg with no value", () => { + const r = runRaw(["resolve", "nextjs", "--renderers-root"]); + expect(r.exitCode).toBe(2); + expect(r.stderr).toMatch(/missing value/i); + }); + + it("breaks priority ties by name ascending (deterministic detect)", () => { + // Two renderers at equal priority, both matching the same config file. + // The alphabetically-first name must win regardless of scan order. + const tmpRoot = mkdtempSync(join(tmpdir(), "rend-tie-")); + try { + const renderersRoot = join(tmpRoot, "renderers"); + const mk = (name) => { + const dir = join(renderersRoot, name); + mkdirSync(dir, { recursive: true }); + writeFileSync( + join(dir, "renderer.json"), + JSON.stringify({ + name, + language: "react", + detect: { configFiles: ["shared.config.*"], dependencies: [], priority: 5 }, + }), + ); + }; + // Create in non-alphabetical order to prove sorting, not insertion order. + mk("zeta"); + mk("alpha"); + + const proj = join(tmpRoot, "proj"); + mkdirSync(proj, { recursive: true }); + writeFileSync(join(proj, "shared.config.ts"), ""); + + const stdout = execFileSync( + "node", + [SCRIPT, "detect", proj, "--json", "--renderers-root", renderersRoot], + { encoding: "utf-8", timeout: 30000 }, + ); + expect(JSON.parse(stdout).renderer).toBe("alpha"); + } finally { + rmSync(tmpRoot, { recursive: true, force: true }); + } + }); }); diff --git a/scripts/__tests__/validate-renderer.test.js b/scripts/__tests__/validate-renderer.test.js index 43dd46a..9c5f3b3 100644 --- a/scripts/__tests__/validate-renderer.test.js +++ b/scripts/__tests__/validate-renderer.test.js @@ -104,4 +104,18 @@ describe("validate-renderer.js", () => { const r = run(["--json"]); expect(r.exitCode).toBe(2); }); + + it("exits 2 when --dir is the last arg with no value", () => { + // Call directly (no trailing --root) so --dir is genuinely the last arg. + let exitCode = 0; + let stderr = ""; + try { + execFileSync("node", [SCRIPT, "--dir"], { encoding: "utf-8", timeout: 30000 }); + } catch (e) { + exitCode = e.status; + stderr = e.stderr || ""; + } + expect(exitCode).toBe(2); + expect(stderr).toMatch(/missing value/i); + }); }); diff --git a/scripts/renderer-lib.js b/scripts/renderer-lib.js new file mode 100644 index 0000000..f5eb3b1 --- /dev/null +++ b/scripts/renderer-lib.js @@ -0,0 +1,70 @@ +#!/usr/bin/env node +/** + * renderer-lib.js — shared helpers for the renderer tooling + * (registry / validate). Pure, side-effect-free functions over renderer + * manifests so each CLI stays thin and testable. Mirrors how + * agent-plugin-lib.js factors shared logic out of the agent-plugin CLIs. + * + * A renderer is a directory under the renderers root containing a + * renderer.json manifest (see renderers/renderer.schema.json). + */ +import { readFileSync, existsSync, readdirSync, statSync } from "fs"; +import { join } from "path"; + +/** Read and parse a renderer's renderer.json. Throws if absent. */ +export function loadManifest(dir) { + const p = join(dir, "renderer.json"); + if (!existsSync(p)) throw new Error(`No renderer.json in ${dir}`); + return JSON.parse(readFileSync(p, "utf-8")); +} + +/** Build a catalog { name -> { dir, manifest } } from renderer.json files + * under the renderers root. Skips unreadable or malformed manifests (and + * nameless ones) so one bad renderer can't abort the scan; the validator + * surfaces those separately. */ +export function loadCatalog(root) { + const catalog = {}; + if (!existsSync(root)) return catalog; + for (const entry of readdirSync(root)) { + const dir = join(root, entry); + let isDir; + try { + isDir = statSync(dir).isDirectory(); + } catch { + continue; + } + if (!isDir) continue; + if (!existsSync(join(dir, "renderer.json"))) continue; + let manifest; + try { + manifest = loadManifest(dir); + } catch { + continue; + } + if (!manifest || typeof manifest.name !== "string") continue; + catalog[manifest.name] = { dir, manifest }; + } + return catalog; +} + +/** List absolute paths of renderer directories (those containing renderer.json) + * under a root. Skips unreadable entries; returns [] if the root is missing. */ +export function listRendererDirs(root) { + const dirs = []; + if (!existsSync(root)) return dirs; + for (const entry of readdirSync(root)) { + const full = join(root, entry); + try { + if (statSync(full).isDirectory() && existsSync(join(full, "renderer.json"))) dirs.push(full); + } catch { + /* skip unreadable entry */ + } + } + return dirs; +} + +/** Translate a simple glob (only `*` wildcards) to an anchored regex. */ +export function globToRegExp(glob) { + const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*"); + return new RegExp(`^${escaped}$`); +} diff --git a/scripts/renderer-registry.js b/scripts/renderer-registry.js index c5c2acc..84afb07 100644 --- a/scripts/renderer-registry.js +++ b/scripts/renderer-registry.js @@ -12,22 +12,33 @@ * node scripts/renderer-registry.js detect [--json] * (--renderers-root overrides the renderers root; used by tests) * - * Exit codes: 0 ok · 1 resolution failure · 2 usage/IO error + * Exit codes: 0 ok · 1 invalid/validation failure · 2 usage/unknown-name/IO error */ -import { readFileSync, readdirSync, existsSync, statSync } from "fs"; +import { readFileSync, readdirSync, existsSync } from "fs"; import { join, dirname, resolve, basename } from "path"; import { fileURLToPath } from "url"; +import { loadCatalog, globToRegExp } from "./renderer-lib.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const DEFAULT_RENDERERS_ROOT = resolve(__dirname, "..", "renderers"); +/** Require a value for a flag that expects one; exit 2 if it's the last arg. */ +function requireValue(flag, value) { + if (value === undefined) { + console.error(`Missing value for ${flag}`); + printHelp(); + process.exit(2); + } + return value; +} + function parseArgs(argv) { const out = { cmd: null, arg: null, json: false, renderersRoot: DEFAULT_RENDERERS_ROOT }; const positional = []; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === "--json") out.json = true; - else if (a === "--renderers-root") out.renderersRoot = resolve(argv[++i]); + else if (a === "--renderers-root") out.renderersRoot = resolve(requireValue(a, argv[++i])); else if (a === "-h" || a === "--help") { printHelp(); process.exit(0); @@ -56,41 +67,6 @@ Options: -h, --help Show this message`); } -/** Build a catalog { name -> { dir, manifest } } from renderer.json files - * under the renderers root. Skips unreadable or malformed manifests so one - * bad renderer can't abort the scan. */ -function loadCatalog(root) { - const catalog = {}; - if (!existsSync(root)) return catalog; - for (const entry of readdirSync(root)) { - const dir = join(root, entry); - let isDir; - try { - isDir = statSync(dir).isDirectory(); - } catch { - continue; - } - if (!isDir) continue; - const manifestPath = join(dir, "renderer.json"); - if (!existsSync(manifestPath)) continue; - let manifest; - try { - manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); - } catch { - continue; - } - if (!manifest || typeof manifest.name !== "string") continue; - catalog[manifest.name] = { dir, manifest }; - } - return catalog; -} - -/** Translate a simple glob (only `*` wildcards) to an anchored regex. */ -function globToRegExp(glob) { - const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*"); - return new RegExp(`^${escaped}$`); -} - function emit(json, payload, human) { if (json) console.log(JSON.stringify(payload, null, 2)); else human(); @@ -156,11 +132,15 @@ function main() { /** Find the first matching renderer, highest detect.priority first. A renderer * matches if any configFiles glob matches a file basename in projectDir, or - * any dependencies entry appears in the project's package.json deps/devDeps. */ + * any dependencies entry appears in the project's package.json deps/devDeps. + * Ties on priority are broken by name ascending so results are deterministic + * across platforms and filesystem ordering. */ function detect(catalog, projectDir) { - const ranked = Object.values(catalog).sort( - (a, b) => (b.manifest.detect?.priority ?? 0) - (a.manifest.detect?.priority ?? 0), - ); + const ranked = Object.values(catalog).sort((a, b) => { + const byPriority = (b.manifest.detect?.priority ?? 0) - (a.manifest.detect?.priority ?? 0); + if (byPriority !== 0) return byPriority; + return a.manifest.name.localeCompare(b.manifest.name); + }); let files = []; try { diff --git a/scripts/validate-renderer.js b/scripts/validate-renderer.js index f069522..9620209 100644 --- a/scripts/validate-renderer.js +++ b/scripts/validate-renderer.js @@ -13,25 +13,36 @@ * node scripts/validate-renderer.js --all [--renderers-root ] [--json] * (--root overrides the repo root for resolving template/agent paths) * - * Exit codes: 0 valid · 1 invalid · 2 usage/IO error + * Exit codes: 0 valid · 1 invalid/validation failure · 2 usage/unknown-name/IO error */ -import { readFileSync, existsSync, statSync, readdirSync } from "fs"; +import { readFileSync, existsSync } from "fs"; import { join, dirname, resolve, basename } from "path"; import { fileURLToPath } from "url"; +import { loadManifest, listRendererDirs } from "./renderer-lib.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(__dirname, ".."); const SCHEMA = join(repoRoot, "renderers", "renderer.schema.json"); const VALID_LANGUAGES = ["react", "vue", "svelte", "react-native"]; +/** Require a value for a flag that expects one; exit 2 if it's the last arg. */ +function requireValue(flag, value) { + if (value === undefined) { + console.error(`Missing value for ${flag}`); + printHelp(); + process.exit(2); + } + return value; +} + function parseArgs(argv) { const out = { dir: null, all: false, json: false, renderersRoot: null, root: repoRoot }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; - if (a === "--dir") out.dir = resolve(argv[++i]); + if (a === "--dir") out.dir = resolve(requireValue(a, argv[++i])); else if (a === "--all") out.all = true; - else if (a === "--renderers-root") out.renderersRoot = resolve(argv[++i]); - else if (a === "--root") out.root = resolve(argv[++i]); + else if (a === "--renderers-root") out.renderersRoot = resolve(requireValue(a, argv[++i])); + else if (a === "--root") out.root = resolve(requireValue(a, argv[++i])); else if (a === "--json") out.json = true; else if (a === "-h" || a === "--help") { printHelp(); @@ -78,12 +89,6 @@ function formatSchemaError(err) { return { path, message }; } -function loadManifest(dir) { - const p = join(dir, "renderer.json"); - if (!existsSync(p)) throw new Error(`No renderer.json in ${dir}`); - return JSON.parse(readFileSync(p, "utf-8")); -} - function validateRenderer(dir, validate, root) { const issues = []; const manifest = loadManifest(dir); // may throw -> caller reports it as an (io) issue @@ -127,20 +132,6 @@ function validateRenderer(dir, validate, root) { return { name: manifest.name ?? dirName, dir, ok: issues.length === 0, issues }; } -function listRendererDirs(root) { - const dirs = []; - if (!existsSync(root)) return dirs; - for (const entry of readdirSync(root)) { - const full = join(root, entry); - try { - if (statSync(full).isDirectory() && existsSync(join(full, "renderer.json"))) dirs.push(full); - } catch { - /* skip unreadable entry */ - } - } - return dirs; -} - async function main() { const args = parseArgs(process.argv.slice(2)); if (!args.dir && !args.all) { From a286cf07b04a3a14c4199c3bade035c5671f5a0f Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:50:08 -0400 Subject: [PATCH 09/26] feat(renderers): author nextjs/vite/sveltekit/expo manifests Co-Authored-By: Claude Opus 4.8 (1M context) --- renderers/expo/renderer.json | 33 +++++++++++++++++++++++++++++++ renderers/nextjs/renderer.json | 30 ++++++++++++++++++++++++++++ renderers/sveltekit/renderer.json | 30 ++++++++++++++++++++++++++++ renderers/vite/renderer.json | 30 ++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 renderers/expo/renderer.json create mode 100644 renderers/nextjs/renderer.json create mode 100644 renderers/sveltekit/renderer.json create mode 100644 renderers/vite/renderer.json diff --git a/renderers/expo/renderer.json b/renderers/expo/renderer.json new file mode 100644 index 0000000..31d40ea --- /dev/null +++ b/renderers/expo/renderer.json @@ -0,0 +1,33 @@ +{ + "name": "expo", + "language": "react-native", + "detect": { + "configFiles": ["app.json", "app.config.*"], + "dependencies": ["expo"], + "priority": 25 + }, + "template": "templates/expo", + "component": { + "ext": ".tsx", + "dir": "src/components", + "pageRouting": "screens" + }, + "test": { + "runner": "jest", + "library": "@testing-library/react-native" + }, + "converter": "react-native-converter", + "commands": { + "dev": "pnpm expo start", + "build": "pnpm expo export", + "test": "pnpm jest" + }, + "phases": { + "exclude": ["visual-diff", "cross-browser", "responsive", "dark-mode"] + }, + "capabilities": { + "islands": false, + "ssr": false, + "darkMode": false + } +} diff --git a/renderers/nextjs/renderer.json b/renderers/nextjs/renderer.json new file mode 100644 index 0000000..33e4839 --- /dev/null +++ b/renderers/nextjs/renderer.json @@ -0,0 +1,30 @@ +{ + "name": "nextjs", + "language": "react", + "detect": { + "configFiles": ["next.config.*"], + "dependencies": ["next"], + "priority": 20 + }, + "template": "templates/nextjs", + "component": { + "ext": ".tsx", + "dir": "src/components", + "pageRouting": "app-router" + }, + "test": { + "runner": "vitest", + "library": "@testing-library/react" + }, + "converter": "figma-react-converter", + "commands": { + "dev": "pnpm dev", + "build": "pnpm build", + "test": "pnpm vitest run" + }, + "capabilities": { + "islands": false, + "ssr": true, + "darkMode": true + } +} diff --git a/renderers/sveltekit/renderer.json b/renderers/sveltekit/renderer.json new file mode 100644 index 0000000..054b033 --- /dev/null +++ b/renderers/sveltekit/renderer.json @@ -0,0 +1,30 @@ +{ + "name": "sveltekit", + "language": "svelte", + "detect": { + "configFiles": ["svelte.config.*"], + "dependencies": ["@sveltejs/kit"], + "priority": 15 + }, + "template": "templates/sveltekit", + "component": { + "ext": ".svelte", + "dir": "src/lib/components", + "pageRouting": "file-based" + }, + "test": { + "runner": "vitest", + "library": "@testing-library/svelte" + }, + "converter": "svelte-converter", + "commands": { + "dev": "pnpm dev", + "build": "pnpm build", + "test": "pnpm vitest run" + }, + "capabilities": { + "islands": false, + "ssr": true, + "darkMode": true + } +} diff --git a/renderers/vite/renderer.json b/renderers/vite/renderer.json new file mode 100644 index 0000000..b188769 --- /dev/null +++ b/renderers/vite/renderer.json @@ -0,0 +1,30 @@ +{ + "name": "vite", + "language": "react", + "detect": { + "configFiles": ["vite.config.*"], + "dependencies": ["vite"], + "priority": 5 + }, + "template": "templates/vite", + "component": { + "ext": ".tsx", + "dir": "src/components", + "pageRouting": "file-based" + }, + "test": { + "runner": "vitest", + "library": "@testing-library/react" + }, + "converter": "figma-react-converter", + "commands": { + "dev": "pnpm dev", + "build": "pnpm build", + "test": "pnpm vitest run" + }, + "capabilities": { + "islands": false, + "ssr": false, + "darkMode": true + } +} From 50aa7b4a6848ba636f92e6a30989b9781ce22d09 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:50:39 -0400 Subject: [PATCH 10/26] ci(renderers): wire validate-renderer into verify-all Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/verify-all.sh | 1 + scripts/verify-renderers.sh | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 scripts/verify-renderers.sh diff --git a/scripts/verify-all.sh b/scripts/verify-all.sh index 9635d9e..eb8e9d7 100644 --- a/scripts/verify-all.sh +++ b/scripts/verify-all.sh @@ -69,6 +69,7 @@ ALL_CHECKS=( "security|./scripts/check-security.sh|" "bundle-size|./scripts/check-bundle-size.sh|" "agent-plugins|./scripts/verify-agent-plugins.sh|" + "renderers|./scripts/verify-renderers.sh|" ) if [[ "$LIST_ONLY" == "true" ]]; then diff --git a/scripts/verify-renderers.sh b/scripts/verify-renderers.sh new file mode 100644 index 0000000..a7cd426 --- /dev/null +++ b/scripts/verify-renderers.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# verify-renderers.sh — Validate every renderer manifest under renderers/. +# Used as the `renderers` check in verify-all.sh. +# Exits non-zero if any manifest fails validation. +set -uo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +node "$DIR/validate-renderer.js" --all "$@" || exit 1 +exit 0 From a4582f6d6e6c60da23ba90c0c0895cac39976afb Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:54:09 -0400 Subject: [PATCH 11/26] refactor(intake): detect framework via renderer-registry, not hardcoded sniffing Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/canva-intake/SKILL.md | 30 +++++++++++------------ .claude/skills/figma-intake/SKILL.md | 30 +++++++++++------------ .claude/skills/screenshot-intake/SKILL.md | 29 ++++++++++------------ 3 files changed, 41 insertions(+), 48 deletions(-) diff --git a/.claude/skills/canva-intake/SKILL.md b/.claude/skills/canva-intake/SKILL.md index 42dd252..450d306 100644 --- a/.claude/skills/canva-intake/SKILL.md +++ b/.claude/skills/canva-intake/SKILL.md @@ -65,16 +65,14 @@ Feed each exported screenshot to Claude for structural analysis: **Simultaneously scan the local project** (identical to figma-intake): ``` -1. Detect framework: - - next.config.* → Next.js (outputTarget: "react") - - vite.config.* + vue in package.json → Vue + Vite (outputTarget: "vue") - - nuxt.config.* → Nuxt (outputTarget: "vue") - - svelte.config.* → SvelteKit (outputTarget: "svelte") - - vite.config.* + svelte in package.json → Svelte + Vite (outputTarget: "svelte") - - app.json with "expo" → Expo (outputTarget: "react-native") - - vite.config.* → Vite + React (outputTarget: "react") - - remix.config.* → Remix (outputTarget: "react") - - None → New project needed (ask output target question) +1. Detect framework via the renderer registry: + - Run: node scripts/renderer-registry.js detect . --json + - On { "renderer": "", "language": "" }: + set build-spec renderer = + set build-spec outputTarget = (react | vue | svelte | react-native) + - On { "renderer": null }: + no framework detected → New project needed (ask output target question) + Do not hand-sniff config files or package.json deps; the registry owns detection. 2. Detect app type: - manifest.json with "manifest_version" → Chrome Extension @@ -162,12 +160,12 @@ Only ask questions whose answers cannot be derived from the design or local proj > (Only ask if existing project detected) **Question 6 — Output Target (only if no framework detected):** -> What framework should I build this in? -> a) React (Next.js / Vite / Remix) -> b) Vue 3 (Nuxt / Vite) -> c) Svelte (SvelteKit / Vite) -> d) React Native (Expo) -> (Skip if existing project with framework detected — auto-detect from package.json) +> Run `node scripts/renderer-registry.js list --json` and present the returned +> renderer names as the choices (each entry has `name` and `language`): +> "Which renderer should I build this in? [numbered list of registry renderers]" +> - Greenfield React default: `vite`. +> - Set build-spec `renderer` = the chosen name and `outputTarget` = its `language`. +> (Skip if the registry already detected a framework for the existing project.) ### Step 4: Generate build-spec.json diff --git a/.claude/skills/figma-intake/SKILL.md b/.claude/skills/figma-intake/SKILL.md index 44b4663..dc6ad15 100644 --- a/.claude/skills/figma-intake/SKILL.md +++ b/.claude/skills/figma-intake/SKILL.md @@ -35,16 +35,14 @@ Run these Figma MCP calls to gather context automatically: Simultaneously scan the local project: ``` -1. Detect framework: - - next.config.* → Next.js (outputTarget: "react") - - vite.config.* + vue in package.json → Vue + Vite (outputTarget: "vue") - - nuxt.config.* → Nuxt (outputTarget: "vue") - - svelte.config.* → SvelteKit (outputTarget: "svelte") - - vite.config.* + svelte in package.json → Svelte + Vite (outputTarget: "svelte") - - app.json with "expo" → Expo (outputTarget: "react-native") - - vite.config.* → Vite + React (outputTarget: "react") - - remix.config.* → Remix (outputTarget: "react") - - None → New project needed (ask output target question) +1. Detect framework via the renderer registry: + - Run: node scripts/renderer-registry.js detect . --json + - On { "renderer": "", "language": "" }: + set build-spec renderer = + set build-spec outputTarget = (react | vue | svelte | react-native) + - On { "renderer": null }: + no framework detected → New project needed (ask output target question) + Do not hand-sniff config files or package.json deps; the registry owns detection. 2. Detect app type: - manifest.json with "manifest_version" → Chrome Extension @@ -127,12 +125,12 @@ Only ask questions whose answers cannot be derived from the Figma file or local > (Only ask if existing project detected) **Question 6 — Output Target (only if no framework detected):** -> What framework should I build this in? -> a) React (Next.js / Vite / Remix) -> b) Vue 3 (Nuxt / Vite) -> c) Svelte (SvelteKit / Vite) -> d) React Native (Expo) -> (Skip if existing project with framework detected — auto-detect from package.json) +> Run `node scripts/renderer-registry.js list --json` and present the returned +> renderer names as the choices (each entry has `name` and `language`): +> "Which renderer should I build this in? [numbered list of registry renderers]" +> - Greenfield React default: `vite`. +> - Set build-spec `renderer` = the chosen name and `outputTarget` = its `language`. +> (Skip if the registry already detected a framework for the existing project.) **Question 7 — App Type (only if ambiguous):** > I detected this as a [chrome-extension / web-app / pwa]. Is that correct? diff --git a/.claude/skills/screenshot-intake/SKILL.md b/.claude/skills/screenshot-intake/SKILL.md index acb982b..5e70be8 100644 --- a/.claude/skills/screenshot-intake/SKILL.md +++ b/.claude/skills/screenshot-intake/SKILL.md @@ -190,16 +190,14 @@ Store extracted data in working memory for Step 4 (questions) and Step 5 (build- ### Step 3: Local Project Scan ``` -1. Detect framework (in order): - - next.config.* → Next.js (outputTarget: "react") - - remix.config.* → Remix (outputTarget: "react") - - nuxt.config.* → Nuxt (outputTarget: "vue") - - svelte.config.* → SvelteKit (outputTarget: "svelte") - - app.json with "expo" → Expo (outputTarget: "react-native") - - vite.config.* + vue in deps → Vite+Vue (outputTarget: "vue") - - vite.config.* + svelte in deps → Vite+Svelte (outputTarget: "svelte") - - vite.config.* → Vite+React (outputTarget: "react") - - None → ask user (Q3 below) +1. Detect framework via the renderer registry: + - Run: node scripts/renderer-registry.js detect . --json + - On { "renderer": "", "language": "" }: + set build-spec renderer = + set build-spec outputTarget = (react | vue | svelte | react-native) + - On { "renderer": null }: + no framework detected → ask user (Q3 below) + Do not hand-sniff config files or package.json deps; the registry owns detection. 2. Detect app type: - manifest.json with "manifest_version" → chrome-extension @@ -240,12 +238,11 @@ Only ask what cannot be derived from captures or local scan. Use `AskUserQuestio > Since detection is vision-based, low-confidence rows are worth a second look. **Q3 — Output target (only if no framework detected):** -> What framework should I build this in? -> a) React (Next.js / Vite / Remix) -> b) Vue 3 (Nuxt / Vite) -> c) Svelte (SvelteKit / Vite) -> d) React Native (Expo) -> Default: React + Vite for new projects. +> Run `node scripts/renderer-registry.js list --json` and present the returned +> renderer names as the choices (each entry has `name` and `language`): +> "Which renderer should I build this in? [numbered list of registry renderers]" +> Default: `vite` (React) for new projects. +> Set build-spec `renderer` = the chosen name and `outputTarget` = its `language`. **Q4 — Component reuse (only if existing components detected):** > I found N existing components that match detected components. From 5695acabcdd27fb583f5d4c0fe04bb0be0ecc394 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 17:56:03 -0400 Subject: [PATCH 12/26] feat(build-spec): add renderer field, deprecate framework.type Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/canva-intake/SKILL.md | 11 ++++- .claude/skills/figma-intake/SKILL.md | 11 ++++- .claude/skills/screenshot-intake/SKILL.md | 11 ++++- .../canva-dashboard.build-spec.json | 1 + .../canva-landing-page.build-spec.json | 1 + .../canva-nested-groups.build-spec.json | 1 + docs/multi-framework/README.md | 48 ++++++++++++++----- 7 files changed, 65 insertions(+), 19 deletions(-) diff --git a/.claude/skills/canva-intake/SKILL.md b/.claude/skills/canva-intake/SKILL.md index 450d306..c108ef5 100644 --- a/.claude/skills/canva-intake/SKILL.md +++ b/.claude/skills/canva-intake/SKILL.md @@ -176,7 +176,8 @@ Write the spec file that all downstream phases consume: { "version": "1.0.0", "source": "canva", - "outputTarget": "react", // "react" | "vue" | "svelte" | "react-native" + "renderer": "vite", // registry renderer name (from `renderer-registry list`): "nextjs" | "vite" | "sveltekit" | "expo" | ... + "outputTarget": "react", // resolved language; equals the renderer's language. "react" | "vue" | "svelte" | "react-native" "createdAt": "2026-03-18T12:00:00Z", "canva": { "designId": "DAGxyz...", @@ -189,7 +190,7 @@ Write the spec file that all downstream phases consume: }, "appType": "web-app", "framework": { - "type": "vite", // "nextjs-app" | "nextjs-pages" | "vite" | "remix" | "nuxt" | "sveltekit" | "expo" + "type": "vite", // DEPRECATED — superseded by top-level `renderer`. Retained for back-compat only. "version": "6.0.0", "outputDir": "src" }, @@ -255,6 +256,12 @@ Write the spec file that all downstream phases consume: } ``` +**Renderer fields:** +- `renderer` (string) is the authoritative framework field. Valid values are the renderer names from `node scripts/renderer-registry.js list --json` (currently `nextjs`, `vite`, `sveltekit`, `expo`). +- `outputTarget` is retained and **equals the resolved renderer's `language`** (react / vue / svelte / react-native). Set both together from the registry `detect`/`resolve` output. +- `framework.type` is **deprecated** — folded into `renderer`. Keep it only for back-compat with older specs; `renderer` wins on any conflict. +- **Back-compat rule:** a build-spec carrying only `outputTarget` (no `renderer`) resolves to that language's DEFAULT renderer: `react → vite`, `svelte → sveltekit`, `react-native → expo`. (`vue` has no renderer yet — its default is unsupported/future.) + **Key differences from Figma build-spec:** - `source` is `"canva"` instead of `"figma"` - `canva` block replaces `figma` block (different metadata) diff --git a/.claude/skills/figma-intake/SKILL.md b/.claude/skills/figma-intake/SKILL.md index dc6ad15..fe96b96 100644 --- a/.claude/skills/figma-intake/SKILL.md +++ b/.claude/skills/figma-intake/SKILL.md @@ -145,7 +145,8 @@ Write the spec file that all downstream phases consume: { "version": "1.0.0", "source": "figma", // "figma" | "canva" - "outputTarget": "react", // "react" | "vue" | "svelte" | "react-native" + "renderer": "vite", // registry renderer name (from `renderer-registry list`): "nextjs" | "vite" | "sveltekit" | "expo" | ... + "outputTarget": "react", // resolved language; equals the renderer's language. "react" | "vue" | "svelte" | "react-native" "createdAt": "2026-03-16T12:00:00Z", "figma": { "fileKey": "abc123", @@ -154,7 +155,7 @@ Write the spec file that all downstream phases consume: }, "appType": "web-app", // "web-app" | "chrome-extension" | "pwa" "framework": { - "type": "vite", // "nextjs-app" | "nextjs-pages" | "vite" | "remix" | "nuxt" | "sveltekit" | "expo" + "type": "vite", // DEPRECATED — superseded by top-level `renderer`. Retained for back-compat only. "version": "6.0.0", "outputDir": "src" }, @@ -231,6 +232,12 @@ Write the spec file that all downstream phases consume: } ``` +**Renderer fields:** +- `renderer` (string) is the authoritative framework field. Valid values are the renderer names from `node scripts/renderer-registry.js list --json` (currently `nextjs`, `vite`, `sveltekit`, `expo`). +- `outputTarget` is retained and **equals the resolved renderer's `language`** (react / vue / svelte / react-native). Set both together from the registry `detect`/`resolve` output. +- `framework.type` is **deprecated** — folded into `renderer`. Keep it only for back-compat with older specs; `renderer` wins on any conflict. +- **Back-compat rule:** a build-spec carrying only `outputTarget` (no `renderer`) resolves to that language's DEFAULT renderer: `react → vite`, `svelte → sveltekit`, `react-native → expo`. (`vue` has no renderer yet — its default is unsupported/future.) + ### Step 5: Confirm and Proceed Present a summary of the build plan: diff --git a/.claude/skills/screenshot-intake/SKILL.md b/.claude/skills/screenshot-intake/SKILL.md index 5e70be8..5e595c5 100644 --- a/.claude/skills/screenshot-intake/SKILL.md +++ b/.claude/skills/screenshot-intake/SKILL.md @@ -268,7 +268,8 @@ Write `.claude/plans/build-spec.json`. The schema is consumed by `canva-token-in { "version": "1.0.0", "source": "screenshot", - "outputTarget": "react", + "renderer": "vite", // registry renderer name (from `renderer-registry list`): "nextjs" | "vite" | "sveltekit" | "expo" | ... + "outputTarget": "react", // resolved language; equals the renderer's language "createdAt": "2026-05-21T14:30:00Z", "screenshot": { "inputType": "url", @@ -285,7 +286,7 @@ Write `.claude/plans/build-spec.json`. The schema is consumed by `canva-token-in }, "appType": "web-app", "framework": { - "type": "vite", + "type": "vite", // DEPRECATED — superseded by top-level `renderer`. Retained for back-compat only. "version": "6.0.0", "outputDir": "src" }, @@ -369,6 +370,12 @@ Write `.claude/plans/build-spec.json`. The schema is consumed by `canva-token-in **Manual-fallback variant:** set `captureMode` to `"manual"`. +**Renderer fields:** +- `renderer` (string) is the authoritative framework field. Valid values are the renderer names from `node scripts/renderer-registry.js list --json` (currently `nextjs`, `vite`, `sveltekit`, `expo`). +- `outputTarget` is retained and **equals the resolved renderer's `language`** (react / vue / svelte / react-native). Set both together from the registry `detect`/`resolve` output. +- `framework.type` is **deprecated** — folded into `renderer`. Keep it only for back-compat with older specs; `renderer` wins on any conflict. +- **Back-compat rule:** a build-spec carrying only `outputTarget` (no `renderer`) resolves to that language's DEFAULT renderer: `react → vite`, `svelte → sveltekit`, `react-native → expo`. (`vue` has no renderer yet — its default is unsupported/future.) + ### Step 6: Confirm and Proceed Present a build plan summary and wait for confirmation: diff --git a/.claude/test-fixtures/canva-dashboard.build-spec.json b/.claude/test-fixtures/canva-dashboard.build-spec.json index 6e3ddc3..ca07d29 100644 --- a/.claude/test-fixtures/canva-dashboard.build-spec.json +++ b/.claude/test-fixtures/canva-dashboard.build-spec.json @@ -1,6 +1,7 @@ { "version": "1.0.0", "source": "canva", + "renderer": "nextjs", "outputTarget": "react", "createdAt": "2026-03-21T00:00:00Z", "canva": { diff --git a/.claude/test-fixtures/canva-landing-page.build-spec.json b/.claude/test-fixtures/canva-landing-page.build-spec.json index d9dc136..5d9aab4 100644 --- a/.claude/test-fixtures/canva-landing-page.build-spec.json +++ b/.claude/test-fixtures/canva-landing-page.build-spec.json @@ -1,6 +1,7 @@ { "version": "1.0.0", "source": "canva", + "renderer": "vite", "outputTarget": "react", "createdAt": "2026-03-21T00:00:00Z", "canva": { diff --git a/.claude/test-fixtures/canva-nested-groups.build-spec.json b/.claude/test-fixtures/canva-nested-groups.build-spec.json index 9f4d354..f22b83a 100644 --- a/.claude/test-fixtures/canva-nested-groups.build-spec.json +++ b/.claude/test-fixtures/canva-nested-groups.build-spec.json @@ -1,6 +1,7 @@ { "version": "1.0.0", "source": "canva", + "renderer": "vite", "outputTarget": "react", "createdAt": "2026-03-21T00:00:00Z", "canva": { diff --git a/docs/multi-framework/README.md b/docs/multi-framework/README.md index 577b10e..d03d6c2 100644 --- a/docs/multi-framework/README.md +++ b/docs/multi-framework/README.md @@ -2,34 +2,56 @@ The pipeline supports generating code for multiple frontend frameworks from any design source (Figma, Canva, or screenshots/URLs). -## The `outputTarget` Field +## The `renderer` and `outputTarget` Fields -The `outputTarget` field in `build-spec.json` controls which framework the pipeline generates code for: +The `renderer` field in `build-spec.json` is the **authoritative** field controlling which framework the pipeline generates code for. Its valid values are the renderer names from the renderer registry (`node scripts/renderer-registry.js list --json`) — currently `nextjs`, `vite`, `sveltekit`, `expo`. ```json { "source": "figma", "appType": "web-app", - "outputTarget": "vue", + "renderer": "vite", + "outputTarget": "react", "components": [...] } ``` -Valid values: `"react"` (default), `"vue"`, `"svelte"`, `"react-native"`. +The `outputTarget` field is **retained** and **equals the resolved `renderer`'s `language`**. Each renderer manifest declares a `language`; resolving a renderer yields the matching `outputTarget`: + +| Renderer | `language` (= `outputTarget`) | +|----------|------------------------------| +| `nextjs` | `react` | +| `vite` | `react` | +| `sveltekit` | `svelte` | +| `expo` | `react-native` | + +Valid `outputTarget` values: `"react"` (default), `"vue"`, `"svelte"`, `"react-native"`. + +> **`framework.type` is deprecated.** It has been folded into `renderer`. Older specs may still carry `framework.type`; it is kept for back-compat only and `renderer` wins on any conflict. + +### Back-compat: resolving `outputTarget` without `renderer` + +A build-spec carrying only `outputTarget` (no `renderer`) resolves to that language's **default renderer**: + +| `outputTarget` | Default renderer | +|----------------|------------------| +| `react` | `vite` | +| `svelte` | `sveltekit` | +| `react-native` | `expo` | +| `vue` | _(no renderer yet — future/unsupported)_ | ## Framework Auto-Detection -If `outputTarget` is not explicitly set during intake, the pipeline detects the framework from the project context: +If `renderer` is not explicitly set during intake, the pipeline detects the framework from the project context via the renderer registry: + +```bash +node scripts/renderer-registry.js detect . --json +# → { "renderer": "", "language": "" } or { "renderer": null } +``` -| Signal | Detected Target | -|--------|----------------| -| `next.config.*` or `react-dom` in `package.json` | `react` | -| `vue` in `package.json` dependencies | `vue` | -| `svelte.config.*` or `svelte` in `package.json` | `svelte` | -| `app.json` with Expo config or `react-native` in `package.json` | `react-native` | -| No project context (greenfield) | `react` (default) | +When a renderer is detected, both `renderer` (the detected name) and `outputTarget` (the resolved `language`) are written to the build-spec. The registry owns all detection logic — the intake skills no longer hand-sniff config files or `package.json` dependencies. When detection returns `null` (greenfield), the intake skills present the registry's renderer list to the user; the greenfield React default is `vite`. -The intake skills (`figma-intake`, `canva-intake`, `screenshot-intake`) also ask the user to confirm or override the detected target during the interview phase. +The intake skills (`figma-intake`, `canva-intake`, `screenshot-intake`) also ask the user to confirm or override the detected renderer during the interview phase. ## Output Targets From 6085286e900b386082d13cd0b931e3fa7bdbc762 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 18:01:10 -0400 Subject: [PATCH 13/26] refactor(pipeline): dispatch Phase-4 converter via renderer manifest Replace hardcoded outputTarget-to-agent dispatch in the three build-from-* commands with reading the build-spec renderer field and resolving its manifest via renderer-registry.js. Phase 4 now dispatches manifest.converter, preferring the source-appropriate React converter (figma/screenshot -> figma-react-converter, canva -> canva-react-converter) when language is react. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/commands/build-from-canva.md | 43 +++++++++++---- .claude/commands/build-from-figma.md | 36 ++++++++++--- .claude/commands/build-from-screenshot.md | 65 +++++++++++++---------- 3 files changed, 96 insertions(+), 48 deletions(-) diff --git a/.claude/commands/build-from-canva.md b/.claude/commands/build-from-canva.md index d71573e..2bbed6d 100644 --- a/.claude/commands/build-from-canva.md +++ b/.claude/commands/build-from-canva.md @@ -39,7 +39,7 @@ Use `TodoWrite` to create a master checklist. Update each item as phases complet [ ] Phase 1: Intake — canva-intake skill → build-spec.json [ ] Phase 2: Token Inference — canva-token-inference skill → lockfile + tailwind config (requires user confirmation) [ ] Phase 3: TDD Scaffold — tdd-from-figma skill → failing tests (RED) -[ ] Phase 4: Component Build — canva-react-converter agent → tests pass (GREEN) +[ ] Phase 4: Component Build — converter agent (per resolved renderer manifest) → tests pass (GREEN) [ ] Phase 4.5: Storybook — generate-stories.sh → auto-generated stories [ ] Phase 5: Visual Verification — pixel-diff loop (max N iterations, against Canva screenshots) [ ] Phase 5.5: Dark Mode — check-dark-mode.sh → dark mode visual verification @@ -104,18 +104,28 @@ Identical to `/build-from-figma` Phase 3. After Phase 3 completes (TDD scaffold with failing tests confirmed), hand off remaining phases to the parallel orchestration skill. +**Read `renderer` from `build-spec.json` and resolve its manifest before dispatching:** + +```bash +node scripts/renderer-registry.js resolve --json +``` + +The manifest drives both converter dispatch (`manifest.converter`) and phase exclusion (`manifest.phases.exclude`). + **Invoke the `parallel-orchestration` skill with:** - Phases to run: `["component-build", "storybook", "visual-diff", "dark-mode", "e2e-tests", "cross-browser", "quality-gate", "responsive", "report"]` + - Drop any phase listed in `manifest.phases.exclude` - Context: - Build spec: `.claude/plans/build-spec.json` - Lockfile: `src/styles/design-tokens.lock.json` - Test files: `src/components/**/*.test.tsx` - Pipeline source: `"canva"` + - Renderer manifest: from `renderer-registry.js resolve --json` - Reference screenshots: `.claude/visual-qa/screenshots/canva/` - Config: `.claude/pipeline.config.json` → `orchestration` section The parallel orchestration skill will: -1. Start `component-build` first (dispatches `canva-react-converter` agent) +1. Start `component-build` first (dispatches the converter named in the resolved manifest, see Phase 4) 2. After build completes, fan out independent phases in parallel (max 3 concurrent) 3. Run `e2e-tests` after `visual-diff` completes 4. Run `report` after both `quality-gate` and `e2e-tests` complete @@ -126,19 +136,30 @@ The parallel orchestration skill will: The individual phase descriptions below serve as reference for what each phase does. The parallel orchestration skill dispatches the same agents, skills, and scripts — it only changes the execution order. -## Phase 4: Component Build +## Phase 4: Component Build (Renderer-Driven) + +Read `renderer` from `build-spec.json`, resolve its manifest, and dispatch the converter it names: + +```bash +node scripts/renderer-registry.js resolve --json +``` + +**Converter selection:** +- If `manifest.language === "react"`, prefer the source-appropriate React converter. For the Canva pipeline that is `canva-react-converter` (it builds React components from Canva screenshots). The shipped React manifests (nextjs, vite) set `converter` to the generic `figma-react-converter`; for the Canva source, use `canva-react-converter` instead. +- Otherwise dispatch `manifest.converter` directly (e.g. `react-native-converter` for expo, and the future `vue-converter` / `svelte-converter`). -Dispatch the `canva-react-converter` agent. +The component extension, directory, page-routing convention, and test command come from `manifest.component` and `manifest.commands.test`. -**Input:** build-spec.json, lockfile, existing test files, Canva screenshots -**Output:** `src/components/**/*.tsx`, page files +**Input:** build-spec.json, resolved renderer manifest, lockfile, existing test files, Canva screenshots +**Output:** Component and page files for the renderer's framework (React: `src/components/**/*.tsx`) This phase: -1. Reads build-spec.json — verifies `source` is `"canva"` -2. References lockfile for all token values (no approximating) -3. Uses Canva screenshots for layout/structure decisions -4. Generates components that satisfy the test files from Phase 3 -5. Runs `pnpm vitest run` after each component batch to confirm GREEN +1. Reads build-spec.json — verifies `source` is `"canva"` and reads `renderer` +2. Resolves the manifest and dispatches the converter (see selection rule above) +3. References lockfile for all token values (no approximating) +4. Uses Canva screenshots for layout/structure decisions +5. Generates components (at `manifest.component.dir` with `manifest.component.ext`) that satisfy the test files from Phase 3 +6. Runs `manifest.commands.test` after each component batch to confirm GREEN **Critical rule:** If tests fail, fix the component — never modify the test files. diff --git a/.claude/commands/build-from-figma.md b/.claude/commands/build-from-figma.md index 45d2bbb..5a28282 100644 --- a/.claude/commands/build-from-figma.md +++ b/.claude/commands/build-from-figma.md @@ -115,18 +115,28 @@ Process components in dependency order: UI primitives → Layout → Sections After Phase 3 completes (TDD scaffold with failing tests confirmed), hand off remaining phases to the parallel orchestration skill. +**Read `renderer` from `build-spec.json` and resolve its manifest before dispatching:** + +```bash +node scripts/renderer-registry.js resolve --json +``` + +The manifest drives both converter dispatch (`manifest.converter`) and phase exclusion (`manifest.phases.exclude`). + **Invoke the `parallel-orchestration` skill with:** - Phases to run: `["component-build", "storybook", "visual-diff", "dark-mode", "e2e-tests", "cross-browser", "quality-gate", "responsive", "report"]` + - Drop any phase listed in `manifest.phases.exclude` - Context: - Build spec: `.claude/plans/build-spec.json` - Lockfile: `src/styles/design-tokens.lock.json` - Test files: `src/components/**/*.test.tsx` - Pipeline source: `"figma"` + - Renderer manifest: from `renderer-registry.js resolve --json` - Figma screenshots: `.claude/visual-qa/screenshots/figma/` - Config: `.claude/pipeline.config.json` → `orchestration` section The parallel orchestration skill will: -1. Start `component-build` first (all other phases depend on it) +1. Start `component-build` first (dispatches the converter named in the resolved manifest, see Phase 4 — all other phases depend on it) 2. After build completes, fan out independent phases in parallel (max 3 concurrent) 3. Run `e2e-tests` after `visual-diff` completes 4. Run `report` after both `quality-gate` and `e2e-tests` complete @@ -137,18 +147,28 @@ The parallel orchestration skill will: The individual phase descriptions below serve as reference for what each phase does. The parallel orchestration skill dispatches the same agents, skills, and scripts — it only changes the execution order. -## Phase 4: Component Build +## Phase 4: Component Build (Renderer-Driven) + +Read `renderer` from `build-spec.json`, resolve its manifest, and dispatch the converter it names: + +```bash +node scripts/renderer-registry.js resolve --json +``` + +**Converter selection:** +- If `manifest.language === "react"`, prefer the source-appropriate React builder. For the Figma pipeline that is `figma-react-converter`, driven by the `figma-to-react-workflow` skill (which detects build-spec.json and lockfile automatically). The shipped React manifests (nextjs, vite) already name `figma-react-converter` as their `converter`. +- Otherwise dispatch `manifest.converter` directly (e.g. `react-native-converter` for expo, and the future `vue-converter` / `svelte-converter`). -Invoke the `figma-to-react-workflow` skill (which detects build-spec.json and lockfile automatically). +The component extension, directory, page-routing convention, and test command come from `manifest.component` and `manifest.commands.test`. -**Input:** build-spec.json, lockfile, existing test files -**Output:** `src/components/**/*.tsx`, page files +**Input:** build-spec.json, resolved renderer manifest, lockfile, existing test files +**Output:** Component and page files for the renderer's framework (React: `src/components/**/*.tsx`) This phase: -1. Skips discovery (build-spec.json exists) +1. Skips discovery (build-spec.json exists); reads `renderer` and resolves its manifest 2. References lockfile for all token values (no approximating) -3. Generates components that satisfy the test files from Phase 3 -4. Runs `pnpm vitest run` after each component batch to confirm GREEN +3. Generates components (at `manifest.component.dir` with `manifest.component.ext`) that satisfy the test files from Phase 3 +4. Runs `manifest.commands.test` after each component batch to confirm GREEN **Critical rule:** If tests fail, fix the component — never modify the test files. diff --git a/.claude/commands/build-from-screenshot.md b/.claude/commands/build-from-screenshot.md index 5d17f1a..65c1234 100644 --- a/.claude/commands/build-from-screenshot.md +++ b/.claude/commands/build-from-screenshot.md @@ -12,7 +12,7 @@ You are the master orchestrator for converting screenshots or a live URL into a - **E2E tests are generated** — Phase 6 generates and runs Playwright E2E tests appropriate to the app type. - **App-type aware** — Chrome extensions, PWAs, and web apps each get tailored test strategies. - **Token inference requires confirmation** — Phase 2 extracts tokens via AI vision and MUST get user confirmation before locking. -- **Output-target aware** — Phase 4 dispatches the correct converter agent based on `build-spec.json.outputTarget`. +- **Renderer-driven** — Phase 4 dispatches the converter agent named by the resolved renderer manifest (`renderer-registry.js resolve `), not a hardcoded `outputTarget` table. ## Input @@ -51,7 +51,7 @@ Use `TodoWrite` to create a master checklist. Update each item as phases complet [ ] Phase 1: Intake — screenshot-intake skill → build-spec.json (with outputTarget) [ ] Phase 2: Token Inference — canva-token-inference skill → lockfile + config (requires user confirmation) [ ] Phase 3: TDD Scaffold — tdd-from-figma skill → failing tests (RED) -[ ] Phase 4: Component Build — converter agent (per outputTarget) → tests pass (GREEN) +[ ] Phase 4: Component Build — converter agent (per resolved renderer manifest) → tests pass (GREEN) [ ] Phase 4.5: Storybook — generate-stories.sh → auto-generated stories [ ] Phase 5: Visual Verification — pixel-diff loop (max N iterations, against source screenshots) [ ] Phase 5.5: Dark Mode — check-dark-mode.sh → dark mode visual verification @@ -116,23 +116,29 @@ Identical to `/build-from-figma` Phase 3. After Phase 3 completes (TDD scaffold with failing tests confirmed), hand off remaining phases to the parallel orchestration skill. -**Read `build-spec.json` to determine `outputTarget` before dispatching.** +**Read `renderer` from `build-spec.json` and resolve its manifest before dispatching:** + +```bash +node scripts/renderer-registry.js resolve --json +``` + +The manifest drives both converter dispatch (`manifest.converter`) and phase exclusion (`manifest.phases.exclude`). **Invoke the `parallel-orchestration` skill with:** - Phases to run: `["component-build", "storybook", "visual-diff", "dark-mode", "e2e-tests", "cross-browser", "quality-gate", "responsive", "report"]` - - For `react-native` outputTarget: exclude `visual-diff`, `dark-mode`, `cross-browser`, `responsive` - - For `chrome-extension` appType: exclude `cross-browser` + - Drop any phase listed in `manifest.phases.exclude` (e.g. expo excludes `visual-diff`, `dark-mode`, `cross-browser`, `responsive`) + - For `chrome-extension` appType: also exclude `cross-browser` - Context: - Build spec: `.claude/plans/build-spec.json` - Lockfile: `src/styles/design-tokens.lock.json` - Test files: `src/components/**/*.test.*` - Pipeline source: `"screenshot"` - - Output target: from `build-spec.json.outputTarget` + - Renderer manifest: from `renderer-registry.js resolve --json` - Reference screenshots: `.claude/visual-qa/screenshots/source/` - Config: `.claude/pipeline.config.json` → `orchestration` section The parallel orchestration skill will: -1. Start `component-build` first (dispatches the correct converter agent per outputTarget) +1. Start `component-build` first (dispatches the converter named in the resolved manifest, see Phase 4) 2. After build completes, fan out independent phases in parallel (max 3 concurrent) 3. Run `e2e-tests` after `visual-diff` completes (or after `component-build` if visual-diff excluded) 4. Run `report` after both `quality-gate` and `e2e-tests` complete @@ -143,29 +149,30 @@ The parallel orchestration skill will: The individual phase descriptions below serve as reference for what each phase does. The parallel orchestration skill dispatches the same agents, skills, and scripts — it only changes the execution order. -## Phase 4: Component Build (Output-Target-Aware) +## Phase 4: Component Build (Renderer-Driven) + +Read `renderer` from `build-spec.json`, resolve its manifest, and dispatch the converter it names: + +```bash +node scripts/renderer-registry.js resolve --json +``` -Read `build-spec.json` and dispatch the correct converter agent based on `outputTarget`: +**Converter selection:** +- If `manifest.language === "react"`, prefer the source-appropriate React converter. For the screenshot pipeline that is `figma-react-converter` (the generic React builder) — it builds React components from reference screenshots regardless of source. The shipped React manifests (nextjs, vite) already set `converter` to `figma-react-converter`. +- Otherwise dispatch `manifest.converter` directly (e.g. `react-native-converter` for the expo renderer, and the future `vue-converter` / `svelte-converter`). -| outputTarget | Agent | Output | -|---|---|---| -| `react` | `canva-react-converter` | `src/components/**/*.tsx`, page files | -| `vue` | `vue-converter` | `src/components/**/*.vue`, page files | -| `svelte` | `svelte-converter` | `src/lib/components/**/*.svelte`, page files | -| `react-native` | `react-native-converter` | `src/components/**/*.tsx` (RN), screen files | +The component file extension, directory, and page-routing convention come from `manifest.component` (`ext`, `dir`, `pageRouting`); the test command comes from `manifest.commands.test`. -**Input:** build-spec.json, lockfile, existing test files, source screenshots -**Output:** Component and page files for the target framework +**Input:** build-spec.json, resolved renderer manifest, lockfile, existing test files, source screenshots +**Output:** Component and page files for the renderer's framework This phase: -1. Reads build-spec.json — verifies `source` is `"screenshot"` and reads `outputTarget` -2. Dispatches the correct converter agent (see table above) +1. Reads build-spec.json — verifies `source` is `"screenshot"` and reads `renderer` +2. Resolves the manifest and dispatches the converter (see selection rule above) 3. References lockfile for all token values (no approximating) 4. Uses source screenshots for layout/structure decisions -5. Generates components that satisfy the test files from Phase 3 -6. Runs the appropriate test command after each component batch to confirm GREEN: - - `react` / `vue` / `svelte`: `pnpm vitest run` - - `react-native`: `pnpm jest` or `pnpm vitest run` (per project config) +5. Generates components (at `manifest.component.dir` with `manifest.component.ext`) that satisfy the test files from Phase 3 +6. Runs `manifest.commands.test` after each component batch to confirm GREEN **Critical rule:** If tests fail, fix the component — never modify the test files. @@ -185,7 +192,7 @@ Same process as `/build-from-figma` Phase 5, but reference screenshots come from For each page: ``` -1. Start: pnpm dev (background) — skip if appType is chrome-extension or outputTarget is react-native +1. Start: pnpm dev (background) — skip if appType is chrome-extension or the renderer excludes `visual-diff` (e.g. expo) 2. Wait for server ready 3. Reference screenshots already exist in .claude/visual-qa/screenshots/source/ @@ -211,17 +218,17 @@ For each page: 5. Stop dev server ``` -**Note:** For `react-native` outputTarget, visual verification is skipped (no browser rendering). Mark as N/A in the checklist. +**Note:** When the resolved renderer excludes `visual-diff` (e.g. expo, which has no browser rendering), this phase is skipped. Mark as N/A in the checklist. ## Phases 5.5 through 9 Identical to `/build-from-figma`. All shared phases work the same regardless of design source: -- **Phase 5.5:** Dark Mode verification (`check-dark-mode.sh`) — skipped for `react-native` +- **Phase 5.5:** Dark Mode verification (`check-dark-mode.sh`) — skipped when the renderer excludes `dark-mode` - **Phase 6:** E2E test generation (`e2e-test-generator` skill) -- **Phase 7:** Cross-browser screenshots (Firefox, WebKit) — skipped for `react-native` +- **Phase 7:** Cross-browser screenshots (Firefox, WebKit) — skipped when the renderer excludes `cross-browser` - **Phase 8:** Quality gate (coverage, types, build, tokens, Lighthouse) -- **Phase 8.5:** Responsive screenshots (`check-responsive.sh`) — skipped for `react-native` +- **Phase 8.5:** Responsive screenshots (`check-responsive.sh`) — skipped when the renderer excludes `responsive` - **Phase 9:** Build report (`.claude/visual-qa/build-report.md`) The build report should note: @@ -236,7 +243,7 @@ The build report should note: - **Screenshot capture fails:** Ask user to manually capture screenshots and provide file paths. - **Image files not found:** List missing files. Ask user to verify paths. - **Token inference low confidence:** Present all tokens with detailed confidence breakdown. Offer to accept user-provided brand guidelines as override. -- **Converter agent missing:** If the requested outputTarget agent does not exist yet, inform the user and offer to fall back to `canva-react-converter` for React output. +- **Converter agent missing:** If the converter named by the resolved manifest does not exist yet, inform the user. For `react` renderers, fall back to `figma-react-converter` (the generic React builder). - **Dev server will not start:** Check for port conflicts, missing dependencies. Run `pnpm install` if needed. - **Tests will not pass after 3 attempts:** Mark component as needing manual intervention, continue with remaining. - **Build fails:** Check TypeScript errors first, then dependency issues. Report blockers. From 11654c993013e5ad457d73647e5d4a4785f0d289 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 18:01:53 -0400 Subject: [PATCH 14/26] refactor(orchestration): exclude phases via manifest.phases.exclude Replace per-framework phase-skipping with a Step 0 that resolves the build-spec renderer manifest and drops every phase listed in manifest.phases.exclude before building the dependency graph (e.g. expo excludes visual-diff/cross-browser/responsive/dark-mode). The component-build dispatch row now selects the converter from the manifest instead of outputTarget. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../skills/parallel-orchestration/SKILL.md | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/.claude/skills/parallel-orchestration/SKILL.md b/.claude/skills/parallel-orchestration/SKILL.md index a638420..77e0663 100644 --- a/.claude/skills/parallel-orchestration/SKILL.md +++ b/.claude/skills/parallel-orchestration/SKILL.md @@ -23,13 +23,28 @@ The scheduler produces real-time streaming output as phases start and finish, an 1. **Phase IDs** -- ordered list of phase IDs to execute (e.g., `["component-build", "storybook", "visual-diff", "dark-mode", "e2e-tests", "cross-browser", "quality-gate", "responsive", "report"]`). 2. **Prior context** -- artifacts from earlier sequential phases: - - `build-spec.json` (component list, appType, outputTarget, E2E flows) + - `build-spec.json` (component list, appType, `renderer`, E2E flows) - `design-tokens.lock.json` (locked token values) - Test files from tdd-scaffold phase 3. **Pipeline config** -- `.claude/pipeline.config.json`, specifically the `orchestration` section. +4. **Renderer manifest** -- the resolved manifest for the build-spec's `renderer`, used to drop excluded phases and select the converter (see Step 0 and Step 5). ## Algorithm +### Step 0: Resolve Renderer and Exclude Phases + +Before scheduling, read `renderer` from `build-spec.json` and resolve its manifest: + +```bash +node scripts/renderer-registry.js resolve --json +``` + +Read `manifest.phases.exclude` (an array, absent/empty for most renderers) and **drop every listed phase from the requested phase set before building the dependency graph.** Excluded phases never enter the scheduler — they are not dispatched, not tracked, and not awaited. + +Example: the `expo` renderer excludes `visual-diff`, `cross-browser`, `responsive`, and `dark-mode` (no browser rendering), so those phases are removed up front and only `component-build`, `storybook`, `e2e-tests`, `quality-gate`, and `report` schedule. When a dependent phase's dependency was excluded, treat that dependency as pre-satisfied (same as a phase completed externally). + +This replaces any per-framework phase-skipping logic — the manifest is the single source of truth for which phases a renderer supports. + ### Step 1: Load Configuration Read `.claude/pipeline.config.json` and extract the `orchestration` section: @@ -125,7 +140,7 @@ function hasResourceConflict(candidatePhase, runningPhases): | Phase | Dispatch Method | |-------|----------------| -| `component-build` | **Agent:** invoke figma-to-react-workflow skill (or vue-converter / svelte-converter / react-native-converter based on `outputTarget` in build-spec.json) | +| `component-build` | **Agent:** dispatch the converter named by the resolved renderer manifest. For React renderers the calling command supplies the source-appropriate converter (figma/screenshot → figma-react-converter via the figma-to-react-workflow skill, canva → canva-react-converter); otherwise dispatch `manifest.converter` (e.g. react-native-converter for expo, future vue-converter / svelte-converter) | | `storybook` | **Bash:** `./scripts/generate-stories.sh` | | `visual-diff` | **Agent:** invoke visual-qa-verification skill with pixel-diff loop (max iterations from `iterationLoop.maxVisualIterations`) | | `dark-mode` | **Bash:** `./scripts/check-dark-mode.sh http://localhost:3000` | @@ -233,10 +248,11 @@ Invoke parallel-orchestration with phases: "e2e-tests", "cross-browser", "quality-gate", "responsive", "report"] Context provided: - - build-spec.json (appType, outputTarget, component list, E2E flows) + - build-spec.json (appType, renderer, component list, E2E flows) - design-tokens.lock.json - Test files from phase 3 - Pipeline config + - Resolved renderer manifest (drives phase exclusion + converter selection) ``` -The parallel scheduler respects the dependency graph within the handed-off phases. For example, `component-build` has no unmet deps (its dep `tdd-scaffold` was completed in the sequential block), so it starts immediately. Phases like `visual-diff`, `storybook`, `dark-mode`, `quality-gate`, `cross-browser`, and `responsive` all depend on `component-build`, so they fan out once it completes. Finally, `report` waits for `quality-gate` and `e2e-tests` before running. +Before scheduling, the scheduler resolves the renderer manifest and drops any phase in `manifest.phases.exclude` (Step 0). The parallel scheduler then respects the dependency graph within the remaining handed-off phases. For example, `component-build` has no unmet deps (its dep `tdd-scaffold` was completed in the sequential block), so it starts immediately. Phases like `visual-diff`, `storybook`, `dark-mode`, `quality-gate`, `cross-browser`, and `responsive` all depend on `component-build`, so they fan out once it completes. Finally, `report` waits for `quality-gate` and `e2e-tests` before running. From 7346767868eadcd2e257888e98da95a18e3e56c0 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 18:02:33 -0400 Subject: [PATCH 15/26] refactor(tokens): derive token config target from renderer manifest Both token skills now resolve the build-spec renderer manifest and choose the config target from manifest.language (react/vue/svelte -> Tailwind CSS-var approach; react-native -> NativeWind) and manifest.template (config location). Astro reuses the React/Tailwind path. Token-extraction logic is unchanged; only the framework-target selection moved from outputTarget branching to the manifest. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/canva-token-inference/SKILL.md | 21 ++++++++++++------- .claude/skills/design-token-lock/SKILL.md | 17 +++++++++++++-- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.claude/skills/canva-token-inference/SKILL.md b/.claude/skills/canva-token-inference/SKILL.md index 94dae22..c18f64e 100644 --- a/.claude/skills/canva-token-inference/SKILL.md +++ b/.claude/skills/canva-token-inference/SKILL.md @@ -263,13 +263,20 @@ After user confirmation, write `src/styles/design-tokens.lock.json` in the same } ``` -### Step 6: Generate Tailwind Config and CSS Properties +### Step 6: Generate Framework Config and CSS Properties -Identical to `design-token-lock` Steps 4-5: +The token-extraction above is framework-agnostic; only the config target changes. Resolve the build-spec's `renderer` manifest to pick the target: -1. Generate or update `tailwind.config.ts` from the lockfile -2. Generate `src/styles/tokens.css` with CSS custom properties -3. All values reference `var(--color-*)` — no raw hex in config +```bash +node scripts/renderer-registry.js resolve --json +``` + +Use `manifest.language` to choose the styling approach and `manifest.template` to locate where the framework's config lives and what shape it takes: + +- **`react` / `vue` / `svelte`** — Tailwind CSS-variable approach. Generate or update the Tailwind config (e.g. `tailwind.config.ts`, alongside `manifest.template`) plus `src/styles/tokens.css`. All values reference `var(--color-*)` — no raw hex in config. (Astro reuses this React/Tailwind path: its language is `react`.) +- **`react-native`** — NativeWind. Generate the NativeWind/Tailwind config and a StyleSheet token module for the Expo template (`manifest.template`) instead of a web `tokens.css`. + +Steps otherwise mirror `design-token-lock` Steps 4-5. ### Step 7: Validate Lockfile @@ -287,8 +294,8 @@ Report any gaps to the user before proceeding. | File | Purpose | |------|---------| | `src/styles/design-tokens.lock.json` | Versioned lockfile — identical format to Figma path | -| `tailwind.config.ts` | Tailwind theme extended from lockfile | -| `src/styles/tokens.css` | CSS custom properties from lockfile | +| Framework config (`tailwind.config.ts` for react/vue/svelte/astro; NativeWind config for react-native) | Theme extended from lockfile, target chosen from `manifest.language` | +| `src/styles/tokens.css` (web) / StyleSheet token module (react-native) | CSS custom properties from lockfile, or RN token module | ## Accuracy Expectations diff --git a/.claude/skills/design-token-lock/SKILL.md b/.claude/skills/design-token-lock/SKILL.md index 5aadee2..cfbf3e1 100644 --- a/.claude/skills/design-token-lock/SKILL.md +++ b/.claude/skills/design-token-lock/SKILL.md @@ -177,9 +177,22 @@ Write `src/styles/design-tokens.lock.json`: } ``` -### Step 4: Generate Tailwind Config +### Step 4: Generate Framework Config -Generate or update `tailwind.config.ts` from the lockfile: +The lockfile and token extraction above are framework-agnostic; only the config target changes. Resolve the build-spec's `renderer` manifest to select it: + +```bash +node scripts/renderer-registry.js resolve --json +``` + +Use `manifest.language` to choose the styling approach and `manifest.template` to locate where the framework's config lives: + +- **`react` / `vue` / `svelte`** — Tailwind CSS-variable approach (the path documented here). Generate or update the Tailwind config (e.g. `tailwind.config.ts`, alongside `manifest.template`). Astro reuses this same React/Tailwind path (its language is `react`). +- **`react-native`** — NativeWind: generate the NativeWind/Tailwind config plus a StyleSheet token module for the Expo template instead of a web `tokens.css`. + +The Figma pipeline targets React renderers (nextjs, vite), so in practice this resolves to the Tailwind path below. + +Generate or update the Tailwind config from the lockfile: ```typescript // Read design-tokens.lock.json and map to Tailwind theme From 51f67d2309273c03cdd3f8580ca93974f40bfcab Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 18:03:14 -0400 Subject: [PATCH 16/26] refactor(tdd): select test runner/library from manifest.test Replace the outputTarget -> test runner/library mapping with reading manifest.test (runner, library, setup, containerApi) from the resolved renderer manifest. Add a forward-looking Astro test split (islands -> Vitest + RTL, static .astro -> Vitest + Container API, page interactivity -> Playwright E2E) for when the astro manifest is authored. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/tdd-from-figma/SKILL.md | 57 +++++++++++++++++++------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/.claude/skills/tdd-from-figma/SKILL.md b/.claude/skills/tdd-from-figma/SKILL.md index 64ef15d..d2c33e7 100644 --- a/.claude/skills/tdd-from-figma/SKILL.md +++ b/.claude/skills/tdd-from-figma/SKILL.md @@ -435,11 +435,24 @@ describe("Service Worker", () => { 4. ALWAYS: Generate standard component tests (existing behavior) ``` -## Output-Target-Aware Test Generation +## Renderer-Aware Test Generation -Read `build-spec.json` field `outputTarget` to determine test library and component patterns. Default is `"react"` (existing behavior above). +Resolve the build-spec's `renderer` manifest to determine the test runner and library: -### Vue 3 Tests (outputTarget: "vue") +```bash +node scripts/renderer-registry.js resolve --json +``` + +Read `manifest.test`: + +- `runner` — `vitest` or `jest` +- `library` — e.g. `@testing-library/react`, `@vue/test-utils`, `@testing-library/svelte`, `@testing-library/react-native` +- `setup` (optional) — test setup file to wire up +- `containerApi` (optional) — when `true`, the framework supports server/static container rendering (used by Astro static `.astro` components; render via the framework's Container API rather than a DOM testing library) + +The component patterns below are grouped by `manifest.language`. Default is React (`@testing-library/react` + Vitest, the existing behavior above). + +### Vue 3 Tests (language: "vue", library: @vue/test-utils) Use `@vue/test-utils` + Vitest: @@ -533,7 +546,7 @@ it("renders named slots", () => { }); ``` -### Svelte Tests (outputTarget: "svelte") +### Svelte Tests (language: "svelte", library: @testing-library/svelte) Use `@testing-library/svelte` + Vitest: @@ -602,9 +615,9 @@ it("navigation has correct landmark", () => { }); ``` -### React Native Tests (outputTarget: "react-native") +### React Native Tests (language: "react-native", runner: jest, library: @testing-library/react-native) -Use `@testing-library/react-native` + Jest (Expo default): +Use `@testing-library/react-native` + Jest (Expo default — `manifest.test.runner` is `jest`): #### Rendering Tests ```typescript @@ -680,18 +693,34 @@ it("renders primary variant styling", () => { ### Conditional Test Generation Logic (Updated) ``` -1. Read build-spec.json → outputTarget, appType -2. Select test library based on outputTarget: - - "react" → @testing-library/react + vitest (existing) - - "vue" → @vue/test-utils + vitest - - "svelte" → @testing-library/svelte + vitest - - "react-native" → @testing-library/react-native + jest -3. Generate component tests using the appropriate library patterns -4. THEN apply app-type-specific tests (existing chrome-extension/pwa logic) +1. Read build-spec.json → renderer, appType +2. Resolve the renderer manifest: renderer-registry.js resolve --json +3. Select the test runner + library directly from manifest.test: + - runner: manifest.test.runner (vitest | jest) + - library: manifest.test.library (@testing-library/react | @vue/test-utils | + @testing-library/svelte | @testing-library/react-native | ...) + - setup: manifest.test.setup (wire up if present) + - containerApi: manifest.test.containerApi (static container rendering, see Astro note) +4. Generate component tests using the patterns for manifest.language +5. THEN apply app-type-specific tests (existing chrome-extension/pwa logic) - Note: chrome-extension and pwa app types only apply to web targets (react/vue/svelte) - react-native does not use chrome-extension or pwa test templates ``` +### Astro Test Split (forward-looking) + +Astro is not yet authored as a renderer; when its manifest lands, `manifest.language` +will be `react` and `manifest.test.containerApi` will be `true`. Split tests by what is +under test: + +- **Islands** (interactive React/Vue/Svelte components) → the island's framework + runner + library from `manifest.test` (e.g. Vitest + `@testing-library/react`). +- **Static `.astro` components** → Vitest + the Astro Container API + (`manifest.test.containerApi === true`): render the component to a string/fragment + and assert on the output rather than using a DOM testing library. +- **Page-level interactivity** (hydration, cross-island behavior) → Playwright E2E, + generated in Phase 6 (e2e-test-generator), not here. + ## Output | File | Purpose | From 577bef7c92ac8da22a51e0f86ddb0fdbe68e94fe Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 18:03:46 -0400 Subject: [PATCH 17/26] refactor(pipeline): phase 4.5 skip note reads renderer not outputTarget Follow-up to the Phase-4 renderer dispatch refactor: the build-from-screenshot Storybook (4.5) skip note referenced 'react-native outputTarget'. Reword to the renderer-driven framing (skip when the renderer has no browser story, e.g. expo). Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/commands/build-from-screenshot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/commands/build-from-screenshot.md b/.claude/commands/build-from-screenshot.md index 65c1234..b1eafc0 100644 --- a/.claude/commands/build-from-screenshot.md +++ b/.claude/commands/build-from-screenshot.md @@ -178,7 +178,7 @@ This phase: ## Phase 4.5: Storybook Generation (Non-Blocking) -Identical to `/build-from-figma` Phase 4.5. Skipped for `react-native` outputTarget. +Identical to `/build-from-figma` Phase 4.5. Skipped when the renderer has no browser story (e.g. expo). ```bash ./scripts/generate-stories.sh From 3bb4e2ae3926c15ae132623ed4fd08261f82b575 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 18:10:02 -0400 Subject: [PATCH 18/26] feat(templates): add astro starter (react islands + tailwind + container-api tests) Co-Authored-By: Claude Opus 4.8 (1M context) --- templates/README.md | 22 +++++++++++++++ templates/astro/astro.config.mjs | 17 ++++++++++++ templates/astro/package.json | 32 ++++++++++++++++++++++ templates/astro/src/components/Counter.tsx | 32 ++++++++++++++++++++++ templates/astro/src/components/Hero.astro | 15 ++++++++++ templates/astro/src/pages/index.astro | 21 ++++++++++++++ templates/astro/src/test/setup.ts | 1 + templates/astro/tailwind.config.mjs | 28 +++++++++++++++++++ templates/astro/tsconfig.json | 13 +++++++++ templates/astro/vitest.config.ts | 20 ++++++++++++++ 10 files changed, 201 insertions(+) create mode 100644 templates/astro/astro.config.mjs create mode 100644 templates/astro/package.json create mode 100644 templates/astro/src/components/Counter.tsx create mode 100644 templates/astro/src/components/Hero.astro create mode 100644 templates/astro/src/pages/index.astro create mode 100644 templates/astro/src/test/setup.ts create mode 100644 templates/astro/tailwind.config.mjs create mode 100644 templates/astro/tsconfig.json create mode 100644 templates/astro/vitest.config.ts diff --git a/templates/README.md b/templates/README.md index b6ea4ad..150c194 100644 --- a/templates/README.md +++ b/templates/README.md @@ -74,6 +74,28 @@ Copies shared templates plus Next.js-specific config. Copies shared templates plus Vite-specific config. +## Astro Templates (`astro/`) + +| File | Purpose | +|------|---------| +| `astro.config.mjs` | Astro config with `@astrojs/react` (islands) + `@astrojs/tailwind` | +| `package.json` | Astro + React island deps; Vitest + RTL + jsdom for tests | +| `tsconfig.json` | Extends `astro/tsconfigs/strict`, `jsx: react-jsx` for islands | +| `tailwind.config.mjs` | Tailwind content globs covering `.astro`, `.tsx`, `.ts`, `.md`, `.mdx` | +| `vitest.config.ts` | `getViteConfig` wiring jsdom + Astro Container API testing | +| `src/pages/index.astro` | Example page composing a static `.astro` + a React island | +| `src/components/Hero.astro` | Static, zero-JS component (props via frontmatter `interface Props`) | +| `src/components/Counter.tsx` | Interactive React island, hydrated with `client:*` | +| `src/test/setup.ts` | Imports `@testing-library/jest-dom` | + +### Usage + +```bash +./scripts/setup-project.sh my-app --renderer astro +``` + +Hybrid output: static/presentational components are zero-JS `.astro` files; interactive components are React islands (`.tsx`) hydrated via `client:load`/`client:visible`. Static components are tested with the Astro Container API (`experimental_AstroContainer` from `astro/container`); islands use Vitest + @testing-library/react. + ## Vue 3 Templates (`vue/`) | File | Purpose | diff --git a/templates/astro/astro.config.mjs b/templates/astro/astro.config.mjs new file mode 100644 index 0000000..fd6e8fe --- /dev/null +++ b/templates/astro/astro.config.mjs @@ -0,0 +1,17 @@ +// @ts-check +import { defineConfig } from "astro/config"; +import react from "@astrojs/react"; +import tailwind from "@astrojs/tailwind"; + +// https://astro.build/config +export default defineConfig({ + // React powers the interactive islands (.tsx). Static, presentational + // components stay zero-JS .astro files. Tailwind drives styling for both. + integrations: [ + react(), + tailwind({ + // Tailwind config lives in tailwind.config.mjs; let it own base styles. + applyBaseStyles: true, + }), + ], +}); diff --git a/templates/astro/package.json b/templates/astro/package.json new file mode 100644 index 0000000..7942ab3 --- /dev/null +++ b/templates/astro/package.json @@ -0,0 +1,32 @@ +{ + "name": "my-astro-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint .", + "format": "prettier --write ." + }, + "dependencies": { + "@astrojs/react": "^4.4.2", + "@astrojs/tailwind": "^6.0.2", + "astro": "^5.18.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwindcss": "^3.4.19" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "jsdom": "^29.1.1", + "typescript": "^5.9.3", + "vitest": "^3.2.0" + } +} diff --git a/templates/astro/src/components/Counter.tsx b/templates/astro/src/components/Counter.tsx new file mode 100644 index 0000000..3dcfbed --- /dev/null +++ b/templates/astro/src/components/Counter.tsx @@ -0,0 +1,32 @@ +import { useState } from "react"; + +export interface CounterProps { + /** Starting value for the counter. */ + initial?: number; + /** Accessible label for the increment control. */ + label?: string; +} + +/** + * Interactive React island. Hydrated on the page via a `client:*` directive + * (e.g. `client:load` above the fold, `client:visible` below it). Tested with + * Vitest + @testing-library/react, exactly like the Vite/Next React path. + */ +export default function Counter({ initial = 0, label = "Increment" }: CounterProps) { + const [count, setCount] = useState(initial); + + return ( +
+ + {count} + + +
+ ); +} diff --git a/templates/astro/src/components/Hero.astro b/templates/astro/src/components/Hero.astro new file mode 100644 index 0000000..979e159 --- /dev/null +++ b/templates/astro/src/components/Hero.astro @@ -0,0 +1,15 @@ +--- +// Static, presentational component — zero JavaScript ships to the client. +// Props are typed via the frontmatter `interface Props`. +interface Props { + title: string; + subtitle?: string; +} + +const { title, subtitle } = Astro.props; +--- + +
+

{title}

+ {subtitle &&

{subtitle}

} +
diff --git a/templates/astro/src/pages/index.astro b/templates/astro/src/pages/index.astro new file mode 100644 index 0000000..5ff12d1 --- /dev/null +++ b/templates/astro/src/pages/index.astro @@ -0,0 +1,21 @@ +--- +// A page composes both flavors: zero-JS .astro statics and hydrated React +// islands. The `client:load` directive hydrates Counter on page load; use +// `client:visible` instead for islands below the fold. +import Hero from "../components/Hero.astro"; +import Counter from "../components/Counter.tsx"; +--- + + + + + + Astro Starter + + + +
+ +
+ + diff --git a/templates/astro/src/test/setup.ts b/templates/astro/src/test/setup.ts new file mode 100644 index 0000000..d0de870 --- /dev/null +++ b/templates/astro/src/test/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; diff --git a/templates/astro/tailwind.config.mjs b/templates/astro/tailwind.config.mjs new file mode 100644 index 0000000..97a813c --- /dev/null +++ b/templates/astro/tailwind.config.mjs @@ -0,0 +1,28 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./src/**/*.{astro,tsx,ts,jsx,js,md,mdx}"], + theme: { + extend: { + // Design tokens (from Figma/Canva/screenshot intake) go here. + // Mirror the structure used by templates/shared/tailwind.config.ts so the + // token-lock and converter pipelines emit a consistent shape. + colors: { + // primary: "#...", + // secondary: "#...", + }, + fontFamily: { + // sans: ["Inter", "system-ui", "sans-serif"], + }, + spacing: { + // Custom spacing scale + }, + borderRadius: { + // Custom radii + }, + boxShadow: { + // Custom shadows + }, + }, + }, + plugins: [], +}; diff --git a/templates/astro/tsconfig.json b/templates/astro/tsconfig.json new file mode 100644 index 0000000..92f840f --- /dev/null +++ b/templates/astro/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [".astro/types.d.ts", "src/**/*"], + "exclude": ["dist", "node_modules"] +} diff --git a/templates/astro/vitest.config.ts b/templates/astro/vitest.config.ts new file mode 100644 index 0000000..24219dc --- /dev/null +++ b/templates/astro/vitest.config.ts @@ -0,0 +1,20 @@ +/// +import { getViteConfig } from "astro/config"; + +// Astro's getViteConfig wires the Astro Vite plugin into Vitest so that both +// component flavors can be exercised in one test run: +// • React islands (.tsx) → @testing-library/react + jsdom (identical to the +// Vite/Next React path). +// • Static .astro files → rendered via the Astro Container API +// (`experimental_AstroContainer` from "astro/container"), then asserted +// against the returned HTML string. +// See https://docs.astro.build/en/reference/container-reference/ for the +// Container API and https://docs.astro.build/en/guides/testing/#vitest. +export default getViteConfig({ + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./src/test/setup.ts"], + include: ["src/**/*.{test,spec}.{ts,tsx}"], + }, +}); From a419577f1926ba561f3eb667e7a32426884dbb56 Mon Sep 17 00:00:00 2001 From: Paul Mulligan Date: Thu, 28 May 2026 18:13:43 -0400 Subject: [PATCH 19/26] feat(agents): add astro-converter (hybrid .astro + react islands) Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/AGENT-NAMING-GUIDE.md | 2 +- .claude/CUSTOM-AGENTS-GUIDE.md | 3 +- .claude/agents/astro-converter.md | 178 ++++++++++++++++++++++++++++++ CLAUDE.md | 10 +- CONTRIBUTING.md | 2 +- README.md | 12 +- docs/guides/agent-creation.md | 2 +- docs/multi-framework/README.md | 24 +++- docs/onboarding/README.md | 4 +- docs/onboarding/architecture.md | 7 +- docs/onboarding/quickstart.md | 4 +- docs/react-development/README.md | 4 +- 12 files changed, 227 insertions(+), 25 deletions(-) create mode 100644 .claude/agents/astro-converter.md diff --git a/.claude/AGENT-NAMING-GUIDE.md b/.claude/AGENT-NAMING-GUIDE.md index dc23999..9edbc9d 100644 --- a/.claude/AGENT-NAMING-GUIDE.md +++ b/.claude/AGENT-NAMING-GUIDE.md @@ -4,7 +4,7 @@ ## Naming Convention -All 53 agents use unique, hyphenated names (e.g., `frontend-developer`, `figma-react-converter`). There are no naming conflicts in the current agent set. +All 54 agents use unique, hyphenated names (e.g., `frontend-developer`, `figma-react-converter`). There are no naming conflicts in the current agent set. Agent files live in `.claude/agents/` as `.md`. diff --git a/.claude/CUSTOM-AGENTS-GUIDE.md b/.claude/CUSTOM-AGENTS-GUIDE.md index 825c890..73d1c78 100644 --- a/.claude/CUSTOM-AGENTS-GUIDE.md +++ b/.claude/CUSTOM-AGENTS-GUIDE.md @@ -1,7 +1,7 @@ # Custom Agents Guide **Last Updated:** 2026-03-25 -**Total Agents:** 53 +**Total Agents:** 54 **Location:** `.claude/agents/` Agents are auto-selected by Claude Code based on task context, or you can request one explicitly. @@ -41,6 +41,7 @@ Agents are auto-selected by Claude Code based on task context, or you can reques |-------|---------|-------------| | figma-react-converter | Figma-to-React conversion pipeline orchestration | Converting Figma designs into React components with Tailwind CSS | | canva-react-converter | Canva-to-React conversion from screenshots | Converting Canva designs into React components with Tailwind CSS | +| astro-converter | Design-to-Astro hybrid conversion (zero-JS .astro statics + React islands) | Building Astro apps where most components are static and a few are interactive React islands | | asset-cataloger | Image/asset semantic mapping and validation | Mapping hash-named exports to meaningful names, validating asset usage | ## Testing & QA diff --git a/.claude/agents/astro-converter.md b/.claude/agents/astro-converter.md new file mode 100644 index 0000000..eaeca7c --- /dev/null +++ b/.claude/agents/astro-converter.md @@ -0,0 +1,178 @@ +--- +name: astro-converter +description: Specialized agent for autonomous design-to-Astro conversion using a hybrid model. Emits zero-JS static .astro components for presentational UI and React islands (.tsx) for interactive components, composed under file-based src/pages/*.astro routes. Pairs with outputTarget "react". +tools: Write, Read, MultiEdit, Bash, Grep, Glob, AskUserQuestion, TaskOutput, Edits, KillShell, Skill, Task, TodoWrite, WebFetch, WebSearch, mcp__figma-desktop__get_design_context, mcp__figma-desktop__get_variable_defs, mcp__figma-desktop__get_screenshot, mcp__figma-desktop__get_metadata, mcp__figma__get_design_context, mcp__figma__get_variable_defs, mcp__figma__get_screenshot, mcp__figma__get_metadata +model: opus +permissionMode: bypassPermissions +--- + +You are an elite design-to-Astro conversion specialist. You turn a locked design +(Figma, Canva, or screenshot/URL) into a production-ready Astro app using Astro's +**hybrid islands architecture**: presentational components ship as zero-JS +`.astro` files, and only genuinely interactive components become hydrated React +islands (`.tsx`) with the appropriate `client:*` directive. + +You are the only net-new converter in the renderer system. You are resolved via +the renderer registry when `build-spec.json` carries `renderer: "astro"` +(`outputTarget: "react"`). + +## When to Use + +- The build-spec's `renderer` field is `"astro"` (Phase 4 dispatch resolves the + converter from the renderer manifest: `node scripts/renderer-registry.js + resolve astro --json` → `converter: "astro-converter"`). +- A content-heavy, mostly-static site where shipping minimal JavaScript matters + (marketing pages, docs, blogs, landing pages) but a handful of components are + interactive. + +## Inputs + +1. **`build-spec.json`** — the machine-readable build plan. You read: + - `renderer` (must be `"astro"`) and `outputTarget` (`"react"`). + - `components[]` — each entry's `category`, `props`, `variants`, and the + interactivity signals below. + - `pages[]` — page name, `route`, and the `sections` it composes. + - `businessLogic` — forms, API calls, auth, state management. Any component + touched by business logic is interactive. +2. **Locked design tokens** — `design-tokens.lock.json` (single source of truth). + Translate to `tailwind.config.mjs` `theme.extend.*` exactly as the React + converter does. Zero hardcoded values. +3. **Screenshots** — visual reference for pixel-accurate layout and the + `get_screenshot` fallback when `get_design_context` fails. + +## The Island / Static Decision (core rule) + +For **each** component in `build-spec.json.components`, classify it: + +**Emit a React island (`.tsx`) when ANY of these is true:** +- The component has an `action` field that implies behavior (anything other than + a pure render — e.g. submit, search, toggle, navigate-with-state). +- Its `category` is interactive (e.g. `forms`, controls, menus, modals, tabs, + accordions, carousels, search boxes, anything with local UI state). +- It is referenced by `build-spec.json.businessLogic` (a form field, an API + call trigger, an auth control, or a piece of `stateManagement`). + + Islands reuse the **existing React converter patterns** verbatim: TypeScript + functional components, exported props `interface`, `useState`/`useReducer` for + local state, Tailwind utility classes, semantic HTML, accessibility + attributes, zero hardcoded values. The `.tsx` lives under `src/components/`. + +**Otherwise emit a static `.astro` component (zero JS):** +- Props typed via the frontmatter `interface Props` (`const { ... } = + Astro.props;`). +- No client-side JavaScript whatsoever — no event handlers, no hooks. +- Tailwind classes in the markup. Slots (``) for composition. + +When in doubt, prefer static: a component is only an island if it must run in +the browser. Presentational cards, heroes, navs (without interactive menus), +footers, sections, and feature lists are static `.astro`. + +## Hydration Directives (`client:*`) + +Reference each island from the page with the cheapest correct directive: + +| Directive | Use when | +|-----------|----------| +| `client:load` | Above-the-fold interactivity needed immediately (primary CTA, header search). | +| `client:visible` | Below-the-fold islands — hydrate when scrolled into view (default for most islands). | +| `client:idle` | Non-urgent interactivity that can wait for the main thread to be idle. | +| `client:media` | Interactivity only relevant at certain breakpoints (e.g. a mobile-only menu). | + +Default to `client:visible`; use `client:load` only for above-the-fold islands. +Never hydrate a static `.astro` component (it has no client directive). + +## Pages + +- Pages are **file-based** routes: `src/pages/*.astro` (index → `/`, `about` → + `/about`, matching each `build-spec.pages[].route`). +- A page imports both static `.astro` components and React islands and composes + them. Static components render inline; islands carry a `client:*` directive. +- Shared chrome (head, html/body, global layout) lives in a layout component the + pages import. + +## Tests (read from `manifest.test`: `runner: vitest`, `containerApi: true`) + +Generate a colocated test for every component you emit: + +- **React islands (`.tsx`)** → Vitest + `@testing-library/react` (`render`, + `screen`, `userEvent`), exactly like the Vite/Next React path. Assert + rendering, interaction, and accessibility. +- **Static `.astro` components** → Vitest + the **Astro Container API**. Render + with `experimental_AstroContainer` and assert against the returned HTML + string: + ```ts + import { experimental_AstroContainer as AstroContainer } from "astro/container"; + import { expect, test } from "vitest"; + import Hero from "./Hero.astro"; + + test("renders the title", async () => { + const container = await AstroContainer.create(); + const html = await container.renderToString(Hero, { + props: { title: "Welcome" }, + }); + expect(html).toContain("Welcome"); + }); + ``` +- **Page-level interactivity** (multi-step flows that cross component + boundaries) → Playwright E2E in Phase 6 (`e2e-test-generator`), not unit tests. + +## Autonomous Workflow + +**Phase 1: Discovery (interactive)** +1. Confirm `renderer: "astro"` in the build-spec; resolve the manifest. +2. Extract / load design tokens and write `tailwind.config.mjs`. +3. Classify every component as island vs. static (the rule above). Produce a + table: component → kind (`.astro` | `.tsx`) → `client:*` (islands only). +4. Survey pages and their sections with screenshots. +5. Present the classification + page plan to the user: "Proceed?" + +**Phase 2: Execution (autonomous)** +1. Write `tailwind.config.mjs` from locked tokens. +2. Build static `.astro` components (presentational) with `interface Props`. +3. Build React islands (`.tsx`) reusing React converter component/prop patterns. +4. Build the layout component and compose `src/pages/*.astro`, wiring each + island's `client:*` directive (`client:load` above the fold, `client:visible` + below). +5. Generate tests: RTL for islands, Container API for `.astro`. +6. Work through all components without "should I continue?" prompts; log errors + and continue. + +**Phase 3: Completion** +1. Summarize: components created (split by island/static), tokens mapped, pages + composed, and any issues. +2. Recommend hydration tuning (e.g. promoting a `client:load` to `client:visible` + if it is below the fold) and follow-up E2E flows. + +## Quality Standards + +- **Static-first.** Ship zero JS unless interactivity is required. The whole + point of Astro is minimizing client JavaScript. +- **Correct hydration.** Every island has exactly one `client:*` directive; + no static component has one. +- **Zero hardcoded values.** Colors, spacing, typography, radii, shadows all map + to Tailwind tokens from the lockfile. +- **TypeScript native.** Island props via exported `interface`; static props via + frontmatter `interface Props`. No `any`. +- **Accessible & responsive.** WCAG 2.1 AA, semantic HTML, mobile-first Tailwind + breakpoints, keyboard navigation, focus-visible styles. +- **Tested.** Every component has a colocated test (RTL or Container API). + +## Key Principles + +1. **Hybrid by default** — static `.astro` for presentation, React islands for + interaction. +2. **Signals drive the split** — `action` / interactive `category` / + `businessLogic` → island; otherwise static. +3. **Cheapest correct hydration** — prefer `client:visible`; `client:load` only + above the fold. +4. **Reuse React patterns for islands** — islands are ordinary React converter + output. +5. **Fully autonomous** — work through all components after Phase 1 approval. +6. **Production ready** — accessible, responsive, token-driven, tested. + +--- + +**Agent Version:** 1.0.0 +**Created:** 2026-05-28 +**Model:** Opus (for hybrid island/static interpretation) +**Execution Mode:** Autonomous with Phase 1 classification review diff --git a/CLAUDE.md b/CLAUDE.md index cc48782..921f9db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ The framework is designed for: ``` project-root/ ├── .claude/ # Claude Code configuration -│ ├── agents/ # 53 specialized agents +│ ├── agents/ # 54 specialized agents │ ├── skills/ # 20 React-specific skills │ ├── commands/ # Custom slash commands │ ├── hooks/ # Hook scripts (automated hooks configured in settings.json) @@ -205,15 +205,15 @@ pnpm tsc --noEmit # Type check without emitting --- -### Custom Agents (53 Total) +### Custom Agents (54 Total) -53 specialized agents covering the full product lifecycle: +54 specialized agents covering the full product lifecycle: | Category | Count | Key Agents | |----------|-------|------------| | Engineering | 12 | frontend-developer, backend-architect, rapid-prototyper, test-writer-fixer, error-boundary-architect, migration-specialist, i18n-engineer, animation-optimizer, bundle-analyzer | | Design | 5 | ui-designer, ux-researcher, brand-guardian | -| Design-to-Code | 6 | figma-react-converter, canva-react-converter, asset-cataloger, vue-converter, svelte-converter, react-native-converter | +| Design-to-Code | 7 | figma-react-converter, canva-react-converter, astro-converter, asset-cataloger, vue-converter, svelte-converter, react-native-converter | | Testing & QA | 7 | visual-qa-agent, accessibility-auditor, api-tester, performance-benchmarker | | Product | 3 | sprint-prioritizer, feedback-synthesizer, trend-researcher | | Marketing | 7 | content-creator, growth-hacker, app-store-optimizer | @@ -580,6 +580,6 @@ node scripts/metrics-dashboard.js summary # Quick metrics summary --- **Last Updated:** 2026-05-28 -**Architecture:** 53 agents, 20 skills, 4 plugins + gh CLI, Figma + Canva + Playwright MCP, 38 scripts, 8 hooks +**Architecture:** 54 agents, 20 skills, 4 plugins + gh CLI, Figma + Canva + Playwright MCP, 38 scripts, 8 hooks > **Keeping counts in sync:** When adding or removing agents, skills, scripts, or hooks, update all count references across the project. Search for the old count number in `*.md` files to find all references: `CLAUDE.md`, `README.md`, `CONTRIBUTING.md`, `docs/onboarding/`, `docs/react-development/`, and `.claude/AGENT-NAMING-GUIDE.md`. The agent and skill counts are enforced automatically by `scripts/check-doc-counts.sh` (run in CI and on pre-commit), which recounts `.claude/agents/` and `.claude/skills/` and fails on any documented count that disagrees. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a8d3a2..dc15cbe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -137,7 +137,7 @@ Because the bump is derived from commit history, **conventional commit messages ## Claude Code Agents -Aurelius includes 53 specialized Claude Code agents and 20 skills that automate significant portions of the development workflow — from design-to-code conversion to testing, accessibility, and deployment. +Aurelius includes 54 specialized Claude Code agents and 20 skills that automate significant portions of the development workflow — from design-to-code conversion to testing, accessibility, and deployment. If you have Claude Code installed, these agents and skills are available to you automatically when working in this repository. They can assist with component development, test writing, visual QA, and much more. diff --git a/README.md b/README.md index f0b14c2..075cb08 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A Claude Code-integrated multi-framework app development framework with TypeScri ## What This Framework Provides -- **53 Custom Agents** -- Specialized AI agents for engineering, design, testing, marketing, operations, and more +- **54 Custom Agents** -- Specialized AI agents for engineering, design, testing, marketing, operations, and more - **20 Development Skills** -- Automated workflows for Figma/Canva conversion, TDD, E2E testing, visual QA, state management, forms, auth, animation, SEO, and more - **10-Phase Design-to-React Pipeline** -- Convert Figma or Canva designs into fully working, tested React apps with a single command - **App-Type Awareness** -- Tailored build and test strategies for web apps, Chrome extensions, and PWAs @@ -85,7 +85,7 @@ project-root/ │ ├── canva-to-react/ # Canva pipeline guide │ └── react-development/ # Development standards ├── .claude/ # Claude Code configuration -│ ├── agents/ # 53 custom agents +│ ├── agents/ # 54 custom agents │ ├── skills/ # 20 development skills │ ├── commands/ # Slash commands (/build-from-figma, /lint, /test) │ ├── pipeline.config.json # Pipeline thresholds and app-type definitions @@ -136,7 +136,7 @@ All thresholds and behavior are configurable in `.claude/pipeline.config.json`: - Lighthouse minimums (performance: 80, accessibility: 90) - App-type-specific E2E strategies -## 53 Custom Agents +## 54 Custom Agents Agents are auto-selected by Claude Code based on your task: @@ -144,7 +144,7 @@ Agents are auto-selected by Claude Code based on your task: |----------|-------|------------| | Engineering | 12 | frontend-developer, backend-architect, rapid-prototyper, test-writer-fixer, error-boundary-architect, migration-specialist, i18n-engineer, animation-optimizer, bundle-analyzer | | Design | 5 | ui-designer, ux-researcher, brand-guardian | -| Design-to-Code | 6 | figma-react-converter, canva-react-converter, vue-converter, svelte-converter, react-native-converter, asset-cataloger | +| Design-to-Code | 7 | figma-react-converter, canva-react-converter, astro-converter, vue-converter, svelte-converter, react-native-converter, asset-cataloger | | Testing & QA | 7 | visual-qa-agent, accessibility-auditor, api-tester, performance-benchmarker | | Product | 3 | sprint-prioritizer, feedback-synthesizer, trend-researcher | | Marketing | 7 | content-creator, growth-hacker, app-store-optimizer | @@ -270,14 +270,14 @@ Details: `.claude/PLUGINS-REFERENCE.md` |----------|----------|-------------| | **Developer onboarding** | `docs/onboarding/README.md` | Start here -- quickstart, architecture, configuration, troubleshooting | | Quickstart guide | `docs/onboarding/quickstart.md` | Clone to running project in 10 minutes | -| Architecture overview | `docs/onboarding/architecture.md` | All 53 agents, 20 skills, 3 pipelines, and how they connect | +| Architecture overview | `docs/onboarding/architecture.md` | All 54 agents, 20 skills, 3 pipelines, and how they connect | | Pipeline configuration | `docs/onboarding/pipeline-configuration.md` | Every setting in pipeline.config.json explained | | Troubleshooting FAQ | `docs/onboarding/troubleshooting.md` | Common issues and solutions | | Project instructions | `CLAUDE.md` | Full project config for Claude Code | | Figma pipeline guide | `docs/figma-to-react/README.md` | Pipeline overview and troubleshooting | | React standards | `docs/react-development/README.md` | TypeScript, Tailwind, testing conventions | | Canva pipeline guide | `docs/canva-to-react/README.md` | Canva pipeline overview and troubleshooting | -| Agent catalog | `.claude/CUSTOM-AGENTS-GUIDE.md` | All 53 agents with use cases | +| Agent catalog | `.claude/CUSTOM-AGENTS-GUIDE.md` | All 54 agents with use cases | | Skills catalog | `.claude/skills/README.md` | All 20 skills with triggers | | Plugin reference | `.claude/PLUGINS-REFERENCE.md` | Plugin configuration and commands | | Scripts reference | `scripts/README.md` | All scripts with usage examples | diff --git a/docs/guides/agent-creation.md b/docs/guides/agent-creation.md index ae4504f..1697af3 100644 --- a/docs/guides/agent-creation.md +++ b/docs/guides/agent-creation.md @@ -2,7 +2,7 @@ Agents are specialized Markdown files with YAML frontmatter that live in `.claude/agents/`. Each agent defines a persona, a set of allowed tools, and detailed instructions that shape how Claude Code behaves when the agent is selected. Claude Code reads the `description` field to decide which agent best matches a given task, then loads that agent's instructions as the system prompt for the session. -This framework ships with 53 agents covering engineering, design, testing, marketing, and operations. You can add your own by following the conventions below. +This framework ships with 54 agents covering engineering, design, testing, marketing, and operations. You can add your own by following the conventions below. ## File Location diff --git a/docs/multi-framework/README.md b/docs/multi-framework/README.md index d03d6c2..3c97587 100644 --- a/docs/multi-framework/README.md +++ b/docs/multi-framework/README.md @@ -4,7 +4,7 @@ The pipeline supports generating code for multiple frontend frameworks from any ## The `renderer` and `outputTarget` Fields -The `renderer` field in `build-spec.json` is the **authoritative** field controlling which framework the pipeline generates code for. Its valid values are the renderer names from the renderer registry (`node scripts/renderer-registry.js list --json`) — currently `nextjs`, `vite`, `sveltekit`, `expo`. +The `renderer` field in `build-spec.json` is the **authoritative** field controlling which framework the pipeline generates code for. Its valid values are the renderer names from the renderer registry (`node scripts/renderer-registry.js list --json`) — currently `nextjs`, `vite`, `astro`, `sveltekit`, `expo`. ```json { @@ -22,6 +22,7 @@ The `outputTarget` field is **retained** and **equals the resolved `renderer`'s |----------|------------------------------| | `nextjs` | `react` | | `vite` | `react` | +| `astro` | `react` | | `sveltekit` | `svelte` | | `expo` | `react-native` | @@ -63,6 +64,27 @@ The intake skills (`figma-intake`, `canva-intake`, `screenshot-intake`) also ask - **Templates:** `templates/nextjs/` (Next.js App Router) or `templates/vite/` (Vite + React) - **Component pattern:** Functional components with TypeScript, props interfaces, `children`/`className` passthrough +### Astro (hybrid islands) + +- **Converter agent:** `astro-converter` +- **Renderer:** `astro` (`language`/`outputTarget` = `react`) +- **Styling:** Tailwind CSS via `@astrojs/tailwind` +- **Test library:** Vitest + @testing-library/react (islands) + Astro Container API (`.astro` statics) +- **Template:** `templates/astro/` (`@astrojs/react` islands + `@astrojs/tailwind`) +- **Component pattern:** Hybrid — zero-JS static `.astro` files for presentational components, React islands (`.tsx`) for interactive ones, composed under file-based `src/pages/*.astro` routes. + +The `astro-converter` agent classifies every component from `build-spec.json`: +- A component with an `action`, an interactive `category`, or any + `businessLogic` involvement → a **React island** (`.tsx`), referenced from the + page with a `client:*` directive (`client:load` above the fold, + `client:visible` below). +- Otherwise → a **static `.astro`** component (zero JS), props typed via the + frontmatter `interface Props`. + +Islands are tested with Vitest + @testing-library/react (the React path); +static `.astro` components are tested with the Astro Container API +(`experimental_AstroContainer` from `astro/container`). + ### Vue 3 - **Converter agent:** `vue-converter` diff --git a/docs/onboarding/README.md b/docs/onboarding/README.md index 8b688e3..d210ab4 100644 --- a/docs/onboarding/README.md +++ b/docs/onboarding/README.md @@ -11,7 +11,7 @@ This guide will get you productive with the framework quickly, whether you are b | Document | What You Will Learn | |----------|-------------------| | [Quickstart Guide](quickstart.md) | Clone, install, create your first project, and run your first pipeline in under 10 minutes | -| [Architecture Overview](architecture.md) | How the 53 agents, 20 skills, 3 pipelines, and 8 hooks fit together | +| [Architecture Overview](architecture.md) | How the 54 agents, 20 skills, 3 pipelines, and 8 hooks fit together | | [Pipeline Configuration](pipeline-configuration.md) | Every setting in `pipeline.config.json` explained, with examples | | [Troubleshooting FAQ](troubleshooting.md) | Common issues, error messages, and how to resolve them | | [Framework Guides](../guides/README.md) | Deep dives into design tokens, visual QA, caching, hooks, error recovery, agent creation, and framework-specific workflows | @@ -52,7 +52,7 @@ Optional (for specific workflows): - [Main README](../../README.md) -- Project overview - [Contributing Guide](../../CONTRIBUTING.md) -- Branch naming, PR process, commit conventions -- [Agent Catalog](../../.claude/CUSTOM-AGENTS-GUIDE.md) -- All 53 agents with use cases +- [Agent Catalog](../../.claude/CUSTOM-AGENTS-GUIDE.md) -- All 54 agents with use cases - [Skills Catalog](../../.claude/skills/README.md) -- All 20 skills with triggers - [Plugin Reference](../../.claude/PLUGINS-REFERENCE.md) -- Installed plugins and commands - [Pipeline Config](../../.claude/pipeline.config.json) -- Thresholds and app-type definitions diff --git a/docs/onboarding/architecture.md b/docs/onboarding/architecture.md index 8a2f52d..d76dfc2 100644 --- a/docs/onboarding/architecture.md +++ b/docs/onboarding/architecture.md @@ -15,7 +15,7 @@ This document explains how Aurelius is structured and how its components work to ┌────────────────────┼─────────────────────┐ │ │ │ ┌───────▼──────┐ ┌────────▼────────┐ ┌────────▼────────┐ - │ 53 Agents │ │ 20 Skills │ │ 4 Plugins │ + │ 54 Agents │ │ 20 Skills │ │ 4 Plugins │ │ (specialized │ │ (workflow │ │ (extensions: │ │ task workers) │ │ automation) │ │ memory, git, │ └───────┬──────┘ └────────┬────────┘ │ superpowers) │ @@ -37,7 +37,7 @@ This document explains how Aurelius is structured and how its components work to --- -## Agents (53 Total) +## Agents (54 Total) Agents are specialized Claude Code sub-processes that handle complex, multi-step tasks. Each agent is a markdown file in `.claude/agents/` with frontmatter defining its tools, capabilities, and instructions. Claude Code selects agents automatically based on your task context. @@ -68,7 +68,7 @@ Agents are specialized Claude Code sub-processes that handle complex, multi-step | `visual-storyteller` | Create data visualizations, infographics, presentations | | `whimsy-injector` | Add micro-interactions and delightful UI details (runs proactively after UI changes) | -### Design-to-Code Agents (6) +### Design-to-Code Agents (7) | Agent | Purpose | Output | |-------|---------|--------| @@ -77,6 +77,7 @@ Agents are specialized Claude Code sub-processes that handle complex, multi-step | `vue-converter` | Convert designs to Vue 3 components | Vue 3 + `