From 9a2d7f00a27df8b4242a770d1c86db824a2db3cf Mon Sep 17 00:00:00 2001 From: Amolith Date: Tue, 19 May 2026 12:10:24 -0600 Subject: [PATCH] Squashed 'vendor/impeccable/' changes from 00d48565..642f03d5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 642f03d5 fix(live-server-test): isolate shared server cwd so tests cannot pollute repo bc189488 Improve critique skill reliability e1d3ea0b Detector architecture v2: static engine, benchmarks, lab, and visual contrast (#156) 4af581e2 chore(skill): bump to v3.1.1 + changelog 5f15163c fix(critique-storage): make CLI entry-point check Windows-safe (#155) e4935044 chore: sync bun.lock to jsdom 29.1.1 from #154 de9aa13a ignore talks 1e8356fa fix(cli): pass --copy to npx skills add to avoid symlinking provider dirs (#148) 4027e17f chore: bump jsdom to 29.1.1, drop border-radius shorthand workaround (#154) dc715c73 craft + codex: explicit user gates before code (Codex test fix) 23b6b9cc chore(cli): bump to v2.1.9 + changelog 735a0f4e chore(skill): bump to v3.1.0 + changelog 8cef2969 craft + codex: extract Codex-specific image flow into codex.md afc974d6 shape: restore image-gen announcement + explicit brief confirmation 93a13f98 Critique persistence: per-run snapshots, ignore list, polish reads as signal (#153) c32daaf3 fix(site): update GitHub star count to 27k e7e923c4 Skill + craft cleanup, detector hardening, native subagent pipeline (#152) e587004e Refactor: cleaner top-level directory structure (#138) 2aeac48b chore: track .impeccable/live/config.json for this repo f7ab774f fix(release): read changelog from site/pages/index.astro after Astro migration 8e3d4d2b chore(skill): bump to v3.0.7 + changelog d874af04 feat(live): make live sessions recoverable (#125) 88b82ae5 Remove Tessl skill review workflow (#136) ea930268 docs(skill): apply STYLE.md to source/skills/impeccable (#135) 122a82f7 docs: strip AI prose, add STYLE.md and validateProse (#134) eecdfa12 fix(site): style Astro-rendered
 blocks in prose bodies (#133)
ccf35735 fix(site): restore .prose class on docs and tutorial bodies (#132)
444e4aca Detector: add italic-serif display headline + hero eyebrow chip rules (#127) (#129)
b8f09c81 Migrate site from Bun to Astro (#130)
a312da5e fix(site): update GitHub star count to 23k, add changelog highlight reel
8c4ea9f0 chore(build): refresh harness output dirs for v3.0.6
a08f808e chore(skill): bump to v3.0.6 + changelog
f4b2b1b0 fix(skill): remove lane catalog from live departure mode, reinforce params
64c6df21 fix(detector): contrast checks run on styled  and 
-              
-              
-              
- Inter font - Purple gradient - Generic copy - Cards on cards -
- - -
- After -
-

Introducing

-

Thoughtful Design

-

Every element serves a purpose. Hierarchy guides the eye. Whitespace breathes.

- -
-
- -
- -
-
- - Generic AI Output -
-
- - With Design Skills -
-
- - - - - -
- - -
-
- 01 -

The Foundation

-
-
-

Before commands, before detection, Impeccable teaches your AI real design. Deep reference knowledge across 7 dimensions, loaded every time your AI writes code.

- -
- -
- -
-

Run /impeccable teach once to set your project's design context. Every command benefits.

-
-
-
- - -
- -
- 02 -

The Language

-
-
-

18 commands form a shared vocabulary between you and your AI. Each one encodes a specific design discipline, so you can steer with precision.

- -
- -
- - - -
-
-

Pick any command to see it in action. View cheatsheet →

-
- -
-
-
- - -
-
- 03 -

The Antidote

-
-
-

Every AI model learned from the same templates. Without intervention, they all produce the same predictable mistakes. Impeccable names them, detects them, and teaches the AI to avoid them.

- - - - - -
-
- - -
-
- 04 -

Visual Mode

-
-
-

See design issues highlighted directly on the page. No screenshots, no guesswork. Impeccable’s overlay shows you exactly what’s wrong and where.

- -
-
-
- - - - Live detection overlay -
- -
-
-
- 25 deterministic checks -

No LLM needed. Pattern matching catches purple gradients, overused fonts, nested cards, low contrast, and more.

-
-
- Three ways to use it -

The Chrome extension on any site, embedded in /critique during an AI design review, or standalone via npx impeccable live.

-
- - Impeccable Chrome extension panel listing detected anti-patterns -
- Available now - Chrome extension - Install from Chrome Web Store → -
-
-
-
-
-
- - -
-
- 05 -

Get Started

-
- - -
-
-

1Install the skills Recommended

-

18 commands that steer your AI toward better design, in real time. The full Impeccable experience.

- -
-
-
- - - -
-
-
-
- $ - npx skills add pbakaus/impeccable - -
- Works with Cursor, Claude Code, Gemini CLI, Codex CLI, and more. -
-
-
-
- -
- Then run /impeccable teach to set up your project's design context. -
-
- - -
- Other install methods - -
- Claude Code plugin -
- $ - /plugin marketplace add pbakaus/impeccable - -
- Then open /plugin in Claude Code -
- -
- Manual download all 11 providers - -
-
-
- - -
- -
- -

2Add the CLI Beta

- -
-
-

Scan any file, directory, or live URL for anti-patterns from the terminal. Catches gradient text, AI color palettes, nested cards, low contrast, and 20+ more rules across HTML, CSS, JSX/TSX, Vue, and Svelte. Use it in CI pipelines, pre-commit hooks, or one-off audits to keep AI slop out of production.

- -
-
- $ - npm i -g impeccable - -
- Or use npx impeccable detect src/ directly without installing. -
- - -
-
- - -
- -

3Browser extension

- -
-
-

Click the toolbar icon on any page and every anti-pattern lights up right where it lives: gradient text, purple palettes, nested cards, tiny body text, and the rest. Works on your localhost, staging, production, or anyone else's site. Great for spot-checking competitors, reviewing PRs visually, or just browsing the web with a sharper eye.

- -
-
- - -
- -

4Stay updated

- -
-
-

Keep skills current and follow along with new commands, anti-patterns, and the design thinking behind Impeccable.

- -
-
- $ - npx impeccable skills update - -
- Run periodically to pull the latest skill definitions. -
- - -
-
-
-
- - -
- -
-
- 06 -

What's New

-
- -
-
-
- v2.1 - April 9, 2026 -
-
    -
  • Streamlined from 21 to 18 commands. Removed overlap and confusion: /arrange renamed to /layout, /normalize merged into /polish (design system alignment is now part of the final pass), /onboard merged into /harden (empty states and first-run experiences are part of production readiness), and /extract became /impeccable extract (a sub-mode alongside craft and teach). Every remaining command has a clearly distinct job.
  • -
  • Automatic cleanup of deprecated skills. On first load after updating, the skill detects and removes leftover files from renamed or merged commands. No manual cleanup needed.
  • -
-
- -
-
- v2.0 - April 8, 2026 -
-
    -
  • Renamed frontend-design to impeccable. The core skill now shares its name with the project, and the teach subcommand moved from /teach-impeccable to /impeccable teach. One skill, one namespace.
  • -
  • Data-driven skill rewrite. The core skill was rebuilt against an internal eval framework that runs the same brief through frontier models with and without the skill loaded, then measures how much the output collapses into monoculture. The result: dramatically more font and color diversity, sharper overall design quality, and much stronger Codex support. The biggest unlock was an anti-attractor procedure that forces the model to enumerate and reject its reflex defaults before picking. Validated on gpt-5.4 and Qwen 3.6 Plus across 15 niches.
  • -
  • Anti-pattern detection engine. 25 deterministic rules across typography, color, layout, motion, and quality. Handles oklch, oklab, lch, and lab color formats, CSS variables inside border shorthands, gradient-backed text, and emoji-only nodes.
  • -
  • CLI: npx impeccable detect. Scans HTML, CSS, JSX/TSX, Vue, Svelte, and CSS-in-JS. Framework detection, multi-file import tracking, Puppeteer-backed live URL scanning, CI-ready JSON output, and a --fast regex mode for huge codebases.
  • -
  • Chrome DevTools extension. One-click detection on any page: yours, staging, production, or someone else's. Reads live computed styles, surfaces findings in an interactive panel, and highlights elements on the page. In Chrome Web Store review.
  • -
  • /critique got teeth. Persona sub-agents review in parallel, score against Nielsen's heuristics, run the detector automatically, and open a live browser overlay so you can walk each finding in place.
  • -
  • New ways to create with Impeccable. /shape runs a structured discovery interview about purpose, audience, and goals, then produces a design brief before any code is written. /impeccable craft chains that brief straight into the full implementation flow so you ship a designed feature instead of a reflex card grid.
  • -
  • New docs site. Top-level Docs, Anti-Patterns, and Visual Mode sections. 18 per-skill pages with before/after demos and the canonical SKILL.md inline, two tutorials, and 38 rule cards with inline visual examples.
  • -
  • New harness: Rovo Dev. 11 supported AI tools total.
  • -
-
- -
- View older releases -
-
-
- v1.6.0 - March 18, 2026 -
-
    -
  • New provider: Trae (China + International)
  • -
  • /critique now scores against Nielsen's 10 heuristics, tests with persona archetypes, and assesses cognitive load
  • -
  • /audit now scores 5 dimensions with P0-P3 severity ratings and structured action plans
  • -
  • Improved skill descriptions for better agent auto-discovery
  • -
  • Fixed invalid YAML frontmatter that broke GitHub preview and Codex loading (#67)
  • -
  • Codex CLI now uses correct $ prefix for command references
  • -
-
- -
-
- v1.5.1 - March 17, 2026 -
-
    -
  • /typeset now recommends fixed type scales for app UIs, reserving fluid typography for marketing/content pages
  • -
-
- -
-
- v1.5.0 - March 16, 2026 -
-
    -
  • 3 new skills: /typeset (fix typography), /arrange (fix layout & spacing), /overdrive (technically extraordinary effects, beta)
  • -
  • Skills now auto-gather design context via .impeccable.md. Run /teach-impeccable once, all skills benefit
  • -
  • Deep linking to commands (#cmd-overdrive, etc.)
  • -
-
- -
-
- v1.3.0 - March 12, 2026 -
-
    -
  • Added OpenCode provider support
  • -
  • Added Pi provider support
  • -
  • Recategorized /onboard as an enhancement command
  • -
-
- -
-
- v1.2.0 - March 5, 2026 -
-
    -
  • Added Kiro support (.kiro/skills/)
  • -
  • Restored prefix toggle: download i- prefixed bundles to avoid naming conflicts
  • -
  • Audit and critique skills only suggest real, installed commands
  • -
-
- -
-
- v1.1.0 - March 4, 2026 -
-
    -
  • Unified skills architecture: commands are now skills with user-invocable: true
  • -
  • Added VS Code Copilot and Google Antigravity support (.agents/skills/)
  • -
  • New install flow: npx skills add as primary, universal ZIP as fallback
  • -
  • Added universal ZIP containing all 5 provider directories
  • -
  • Renamed /simplify to /distill to avoid Claude Code conflict
  • -
-
- -
-
- v1.0.0 - February 28, 2026 -
-
    -
  • Initial release with enhanced frontend-design skill
  • -
  • 17 design commands: /polish, /audit, /distill, /bolder, and more
  • -
  • Support for Cursor, Claude Code, Gemini CLI, and Codex CLI
  • -
  • Interactive command cheatsheet
  • -
-
-
-
-
- -
- - -
-
- 07 -

Frequently Asked Questions

-
- -
-
- Where do I put the downloaded files? -
-

The easiest way is npx skills add pbakaus/impeccable, which auto-detects your AI harness and places files correctly.

-

If you downloaded the universal ZIP, extract it to your project root (same level as your package.json or src/ folder). It creates hidden folders for each supported tool: .cursor/, .claude/, .gemini/, .codex/, and .agents/.

-

Project-level installation takes precedence and lets you version control your skills.

-
-
- -
- How do I update to the latest version? -
-

Run npx impeccable skills update from your project root. It downloads the latest skills, cleans up deprecated files, and preserves any prefix you use.

-
    -
  • Alternative: npx skills add pbakaus/impeccable re-installs from scratch.
  • -
  • Claude Code plugin: Open /plugin, go to the Discover tab.
  • -
  • Manual ZIP: Download from above and extract to the project root.
  • -
-

Your .impeccable.md context file is never overwritten.

-
-
- -
- Commands or skills aren't appearing. What do I do? -
-

For commands: Type / in your AI harness and look for commands like /audit, /polish, etc. If they don't appear, double-check the files are in the correct location.

-

For skills: Skills are applied automatically when relevant. To verify, explicitly mention "use the impeccable skill" in your prompt. This forces the AI to acknowledge and apply it.

-

Tool-specific setup:

-
    -
  • Cursor: Requires Nightly channel + Agent Skills enabled in Settings → Rules
  • -
  • Gemini CLI: Requires @google/gemini-cli@preview + Skills enabled via /settings
  • -
-
-
- -
- I'm new to AI harnesses. Where do I start? -
-

Skills and commands are intermediate features. If you're just getting started, learn the basics first:

- -

Once you're comfortable with basic prompting and code generation, come back and give Impeccable a try.

-
-
- -
- Is Impeccable free? -
-

Yes. Everything is Apache 2.0: skills, commands, CLI, and the detection engine. Fully open source, free for everyone.

-
-
-
-
-
- - -
-
-
-

Work with me

-

Impeccable is built by Renaissance Geek. I work with enterprise teams on large-scale rollouts, custom integrations, and training for designers and developers. If you're a frontier lab, design tool company, or enterprise looking to raise the bar on AI-generated design, let's talk.

-
- -
-
-
- - - - - - diff --git a/public/js/generated/counts.js b/public/js/generated/counts.js deleted file mode 100644 index 397efb84cfee19ffe1cac1d6abea5ad3cbbc4b24..0000000000000000000000000000000000000000 --- a/public/js/generated/counts.js +++ /dev/null @@ -1,3 +0,0 @@ -// GENERATED by build.js — do not edit -export const COMMAND_COUNT = 18; -export const DETECTION_COUNT = 25; diff --git a/public/privacy.html b/public/privacy.html deleted file mode 100644 index 6944106c6bdea28143369dffc06520e54a59e14f..0000000000000000000000000000000000000000 --- a/public/privacy.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - Privacy Policy - Impeccable - - - - - - - - - - - - -
-

Privacy Policy

-

Last updated: April 6, 2026

- -

What Impeccable is

-

Impeccable is an open-source collection of agent skills (text files) that run locally in your AI coding tool. The skills themselves collect no data, make no network requests, and have no analytics.

- -

Website analytics

-

The Impeccable website (impeccable.style) uses Google Analytics to understand traffic patterns (page views, referrers, country). No personal information is collected beyond what Google Analytics provides by default. No cookies are used for advertising.

- -

Downloads

-

When you download a skill bundle from the website, we log the download event (which bundle, timestamp) for usage statistics. No personal information is attached to these logs.

- -

Claude Code Plugin

-

When installed as a Claude Code plugin, Impeccable runs entirely within your local Claude Code session. No data is sent to Impeccable's servers. Anthropic's own privacy policy governs the Claude Code application itself.

- -

Chrome Extension

-

The Impeccable Chrome DevTools extension runs entirely in your browser. All anti-pattern detection happens locally on the page you are inspecting. No page content, URLs, or detection results are ever sent to any external server.

-

The extension stores your rule preferences (which detections are enabled or disabled) using Chrome's built-in sync storage (chrome.storage.sync), which syncs settings across your Chrome instances via your Google account. No other data is stored or transmitted.

-

The extension requests the following permissions:

-
    -
  • activeTab / scripting - to inject the detector script into the page you are inspecting
  • -
  • storage - to save your rule preferences
  • -
  • webNavigation - to re-scan automatically when you navigate to a new page
  • -
  • Host permissions (all URLs) - so the detector can run on any website you choose to inspect
  • -
- -

GitHub

-

The source code is hosted on GitHub. Interactions with the repository (issues, pull requests, stars) are governed by GitHub's privacy policy.

- -

Contact

-

Questions about this policy? Open an issue on GitHub or reach out to @pbakaus.

-
- - diff --git a/public/sitemap.xml b/public/sitemap.xml deleted file mode 100644 index 8cc157893ceae2494d87a1426d0bad0d19079dc5..0000000000000000000000000000000000000000 --- a/public/sitemap.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - https://impeccable.style/ - 2026-04-08 - weekly - 1.0 - - - https://impeccable.style/cheatsheet - 2026-04-08 - monthly - 0.7 - - - https://impeccable.style/gallery - 2026-04-08 - monthly - 0.7 - - - - - https://impeccable.style/skills - 2026-04-08 - weekly - 0.9 - - - https://impeccable.style/anti-patterns - 2026-04-08 - weekly - 0.9 - - - https://impeccable.style/tutorials - 2026-04-08 - weekly - 0.9 - - - - https://impeccable.style/skills/adapt2026-04-080.8 - https://impeccable.style/skills/animate2026-04-080.8 - https://impeccable.style/skills/arrange2026-04-080.8 - https://impeccable.style/skills/audit2026-04-080.8 - https://impeccable.style/skills/bolder2026-04-080.8 - https://impeccable.style/skills/clarify2026-04-080.8 - https://impeccable.style/skills/colorize2026-04-080.8 - https://impeccable.style/skills/critique2026-04-080.8 - https://impeccable.style/skills/delight2026-04-080.8 - https://impeccable.style/skills/distill2026-04-080.8 - https://impeccable.style/skills/extract2026-04-080.8 - https://impeccable.style/skills/harden2026-04-080.8 - https://impeccable.style/skills/impeccable2026-04-080.8 - https://impeccable.style/skills/normalize2026-04-080.8 - https://impeccable.style/skills/onboard2026-04-080.8 - https://impeccable.style/skills/optimize2026-04-080.8 - https://impeccable.style/skills/overdrive2026-04-080.8 - https://impeccable.style/skills/polish2026-04-080.8 - https://impeccable.style/skills/quieter2026-04-080.8 - https://impeccable.style/skills/shape2026-04-080.8 - https://impeccable.style/skills/typeset2026-04-080.8 - - - https://impeccable.style/tutorials/getting-started2026-04-080.8 - https://impeccable.style/tutorials/critique-with-overlay2026-04-080.8 - diff --git a/scripts/benchmark-detector.mjs b/scripts/benchmark-detector.mjs new file mode 100644 index 0000000000000000000000000000000000000000..1e43cff4a758d2b0e771fbd13f07f1bad7fc69e4 --- /dev/null +++ b/scripts/benchmark-detector.mjs @@ -0,0 +1,583 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import http from 'node:http'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { + createBrowserDetector, + createDetectorProfile, + detectHtml, + detectText, + detectUrl, + summarizeDetectorProfile, + walkDir, +} from '../cli/engine/detect-antipatterns.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); +const FIXTURES = path.join(ROOT, 'tests', 'fixtures', 'antipatterns'); +const BROWSER_FIXTURES = [ + 'cramped-padding.html', + 'quality.html', + 'body-text-viewport-edge.html', +]; + +const MIME = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', +}; + +function parseArgs(argv) { + const args = { + browser: false, + json: false, + out: null, + quick: false, + }; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--browser') args.browser = true; + else if (arg === '--json') args.json = true; + else if (arg === '--quick') args.quick = true; + else if (arg === '--out') args.out = argv[++i] || null; + else if (arg.startsWith('--out=')) args.out = arg.slice('--out='.length); + else if (arg === '--help') { + printUsage(); + process.exit(0); + } + } + return args; +} + +function printUsage() { + console.log(`Usage: node scripts/benchmark-detector.mjs [options] + +Options: + --quick Run a small smoke benchmark subset + --browser Include browser-backed URL benchmarks + --json Print the benchmark report as JSON + --out FILE Write the benchmark report JSON to FILE + --help Show this help message`); +} + +function nowMs() { + return typeof performance !== 'undefined' && performance.now + ? performance.now() + : Date.now(); +} + +function roundMs(value) { + return Number(value.toFixed(3)); +} + +function isHtml(filePath) { + const ext = path.extname(filePath).toLowerCase(); + return ext === '.html' || ext === '.htm'; +} + +function rel(filePath) { + return path.relative(ROOT, filePath); +} + +function addEvent(profile, event) { + profile.events.push({ + engine: event.engine || 'unknown', + phase: event.phase || 'unknown', + ruleId: event.ruleId || 'unknown', + target: event.target || '', + ms: Number.isFinite(event.ms) ? event.ms : 0, + findings: Number.isFinite(event.findings) ? event.findings : 0, + }); +} + +async function measureCase({ name, engine, mode, target, run }) { + const profile = createDetectorProfile(); + const started = nowMs(); + try { + const result = await run(profile); + const findings = Array.isArray(result) + ? result.length + : (Number.isFinite(result?.findings) ? result.findings : 0); + return { + name, + engine, + mode, + target, + status: 'ok', + totalMs: roundMs(nowMs() - started), + findings, + profile: summarizeDetectorProfile(profile), + events: profile.events, + }; + } catch (err) { + return { + name, + engine, + mode, + target, + status: 'failed', + totalMs: roundMs(nowMs() - started), + findings: 0, + error: err?.message || String(err), + profile: summarizeDetectorProfile(profile), + events: profile.events, + }; + } +} + +function skippedCase({ name, engine, mode, target, reason }) { + return { + name, + engine, + mode, + target, + status: 'skipped', + totalMs: 0, + findings: 0, + skipReason: reason, + profile: [], + events: [], + }; +} + +async function scanDirectory(files, fastMode, profile) { + const findings = []; + for (const file of files) { + if (!fastMode && isHtml(file)) { + findings.push(...await detectHtml(file, { profile })); + } else { + const content = fs.readFileSync(file, 'utf-8'); + findings.push(...detectText(content, file, { profile })); + } + } + return findings; +} + +function selectQuickFiles(files, predicate, preferredNames) { + const preferred = preferredNames + .map(name => files.find(file => path.basename(file) === name)) + .filter(Boolean); + const fallback = files.filter(predicate).slice(0, preferredNames.length || 2); + return preferred.length ? preferred : fallback; +} + +async function runFileBenchmarks(args) { + const files = walkDir(FIXTURES).sort(); + const htmlFiles = files.filter(isHtml); + const textFiles = files.filter(file => !isHtml(file)); + const selectedText = args.quick + ? textFiles.slice(0, 2) + : textFiles; + const selectedHtml = args.quick + ? selectQuickFiles(htmlFiles, isHtml, ['color.html', 'quality.html']) + : htmlFiles; + const directoryFiles = args.quick + ? [...selectedHtml, ...selectedText].sort() + : files; + + const cases = []; + for (const file of selectedText) { + cases.push(await measureCase({ + name: `detectText:${rel(file)}`, + engine: 'regex', + mode: 'file', + target: rel(file), + run: (profile) => detectText(fs.readFileSync(file, 'utf-8'), file, { profile }), + })); + } + + for (const file of selectedHtml) { + cases.push(await measureCase({ + name: `detectHtml:${rel(file)}`, + engine: 'static-html', + mode: 'file', + target: rel(file), + run: (profile) => detectHtml(file, { profile }), + })); + } + + cases.push(await measureCase({ + name: args.quick ? 'directory-default:quick-fixtures' : 'directory-default:all-fixtures', + engine: 'mixed', + mode: 'directory-default', + target: rel(FIXTURES), + run: (profile) => scanDirectory(directoryFiles, false, profile), + })); + + cases.push(await measureCase({ + name: args.quick ? 'directory-fast:quick-fixtures' : 'directory-fast:all-fixtures', + engine: 'regex', + mode: 'directory-fast', + target: rel(FIXTURES), + run: (profile) => scanDirectory(directoryFiles, true, profile), + })); + + return cases; +} + +function startFixtureServer() { + const server = http.createServer((req, res) => { + let filePath; + const urlPath = req.url?.split('?')[0] || '/'; + if (urlPath.startsWith('/fixtures/')) { + filePath = path.join(ROOT, 'tests', urlPath); + } else if (urlPath === '/js/detect-antipatterns-browser.js') { + filePath = path.join(ROOT, 'cli', 'engine', 'detect-antipatterns-browser.js'); + } else { + res.writeHead(404).end(); + return; + } + try { + const body = fs.readFileSync(filePath); + const ext = path.extname(filePath).toLowerCase(); + res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' }); + res.end(body); + } catch { + res.writeHead(404).end(); + } + }); + + return new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + server.off('error', reject); + const address = server.address(); + resolve({ + server, + baseUrl: `http://127.0.0.1:${address.port}`, + }); + }); + }); +} + +async function closeServer(server) { + await new Promise(resolve => server.close(resolve)); +} + +async function runBrowserBenchmarks(args) { + let serverInfo; + try { + serverInfo = await startFixtureServer(); + } catch (err) { + return [ + skippedCase({ + name: 'browser:fixtures', + engine: 'browser', + mode: 'browser', + target: 'localhost', + reason: `localhost fixture server unavailable: ${err?.message || err}`, + }), + ]; + } + + const cases = []; + const browserFiles = args.quick ? ['quality.html'] : BROWSER_FIXTURES; + + try { + for (const fileName of browserFiles) { + const url = `${serverInfo.baseUrl}/fixtures/antipatterns/${fileName}`; + const fresh = await measureCase({ + name: `detectUrl:fresh-load:${fileName}`, + engine: 'browser', + mode: 'fresh-load', + target: url, + run: (profile) => detectUrl(url, { profile, waitUntil: 'load', settleMs: 100 }), + }); + if (fresh.status === 'failed' && /Could not find Chrome|Failed to launch|executable|spawn|puppeteer/i.test(fresh.error || '')) { + cases.push(skippedCase({ + name: `detectUrl:fresh-load:${fileName}`, + engine: 'browser', + mode: 'fresh-load', + target: url, + reason: `Chromium unavailable: ${fresh.error}`, + })); + } else { + cases.push(fresh); + } + } + + const visualContrastUrl = `${serverInfo.baseUrl}/fixtures/antipatterns/visual-contrast.html`; + cases.push(await measureCase({ + name: 'detectUrl:visual-contrast', + engine: 'browser', + mode: 'visual-contrast', + target: visualContrastUrl, + run: (profile) => detectUrl(visualContrastUrl, { + profile, + waitUntil: 'load', + settleMs: 0, + visualContrast: true, + }), + })); + + cases.push(await measureCase({ + name: 'detectUrl:warm-load', + engine: 'browser', + mode: 'warm-load', + target: serverInfo.baseUrl, + run: async (profile) => { + const detector = await createBrowserDetector({ waitUntil: 'load', settleMs: 100 }); + const findings = []; + try { + for (const fileName of browserFiles) { + const url = `${serverInfo.baseUrl}/fixtures/antipatterns/${fileName}`; + findings.push(...await detector.detectUrl(url, { profile })); + } + } finally { + await detector.close(); + } + return findings; + }, + })); + + cases.push(await measureCase({ + name: 'detectUrl:warm-networkidle0', + engine: 'browser', + mode: 'warm-networkidle0', + target: serverInfo.baseUrl, + run: async (profile) => { + const detector = await createBrowserDetector({ waitUntil: 'load', settleMs: 100 }); + const findings = []; + try { + for (const fileName of browserFiles) { + const url = `${serverInfo.baseUrl}/fixtures/antipatterns/${fileName}`; + findings.push(...await detector.detectUrl(url, { + profile, + waitUntil: 'networkidle0', + settleMs: 0, + })); + } + } finally { + await detector.close(); + } + return findings; + }, + })); + + let puppeteer; + try { + puppeteer = await import('puppeteer'); + } catch (err) { + cases.push(skippedCase({ + name: 'browser:pure-vs-overlay', + engine: 'browser', + mode: 'pure-vs-overlay', + target: serverInfo.baseUrl, + reason: `puppeteer unavailable: ${err?.message || err}`, + })); + return cases; + } + + cases.push(await measureCase({ + name: 'browser:pure-vs-overlay', + engine: 'browser', + mode: 'pure-vs-overlay', + target: serverInfo.baseUrl, + run: async (profile) => { + let browser; + const launchStarted = nowMs(); + try { + browser = await puppeteer.default.launch({ + headless: true, + args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [], + }); + addEvent(profile, { + engine: 'browser', + phase: 'load', + ruleId: 'launch-browser-overlay-bench', + target: serverInfo.baseUrl, + ms: nowMs() - launchStarted, + }); + } catch (err) { + throw new Error(`Chromium unavailable: ${err?.message || err}`); + } + + let findings = []; + try { + const page = await browser.newPage(); + const url = `${serverInfo.baseUrl}/fixtures/antipatterns/${browserFiles[0]}`; + const browserScript = fs.readFileSync(path.join(ROOT, 'cli', 'engine', 'detect-antipatterns-browser.js'), 'utf-8'); + await page.setViewport({ width: 1280, height: 800 }); + await page.goto(url, { waitUntil: 'load', timeout: 30000 }); + await new Promise(resolve => setTimeout(resolve, 100)); + await page.evaluate(() => { window.__IMPECCABLE_CONFIG__ = { autoScan: false }; }); + await page.evaluate(browserScript); + const pureStarted = nowMs(); + findings = await page.evaluate(() => { + const serialized = window.impeccableDetect({ decorate: false, serialize: true }); + return serialized.flatMap(({ findings }) => findings.map(f => ({ id: f.type, snippet: f.detail }))); + }); + addEvent(profile, { + engine: 'browser', + phase: 'scan', + ruleId: 'pure-detect', + target: url, + ms: nowMs() - pureStarted, + findings: findings.length, + }); + const overlayStarted = nowMs(); + const overlayGroupCount = await page.evaluate(() => window.impeccableScan().length); + addEvent(profile, { + engine: 'browser', + phase: 'scan', + ruleId: 'overlay-scan', + target: url, + ms: nowMs() - overlayStarted, + findings: overlayGroupCount, + }); + await page.close().catch(() => {}); + } finally { + const closeStarted = nowMs(); + await browser.close().catch(() => {}); + addEvent(profile, { + engine: 'browser', + phase: 'load', + ruleId: 'close-browser-overlay-bench', + target: serverInfo.baseUrl, + ms: nowMs() - closeStarted, + }); + } + return findings; + }, + })); + } finally { + await closeServer(serverInfo.server); + } + + return cases.map(testCase => { + if (testCase.engine === 'browser' && testCase.status === 'failed' && /Chromium unavailable|Failed to launch|Could not find Chrome|executable|spawn|puppeteer/i.test(testCase.error || '')) { + return skippedCase({ + name: testCase.name, + engine: testCase.engine, + mode: testCase.mode, + target: testCase.target, + reason: testCase.error, + }); + } + return testCase; + }); +} + +function aggregateEvents(cases) { + const profile = createDetectorProfile(); + for (const testCase of cases) { + if (Array.isArray(testCase.events)) profile.events.push(...testCase.events); + } + return summarizeDetectorProfile(profile); +} + +function makeReport(args, cases) { + const summary = aggregateEvents(cases); + return { + version: 1, + createdAt: new Date().toISOString(), + cwd: ROOT, + quick: args.quick, + browser: args.browser, + cases: cases.map(({ events, ...testCase }) => testCase), + summary, + }; +} + +function pad(value, width) { + const str = String(value); + if (str.length >= width) return str.slice(0, width); + return str + ' '.repeat(width - str.length); +} + +function printRows(rows, columns) { + const header = columns.map(col => pad(col.label, col.width)).join(' '); + console.log(header); + console.log(columns.map(col => '-'.repeat(col.width)).join(' ')); + for (const row of rows) { + console.log(columns.map(col => pad(row[col.key] ?? '', col.width)).join(' ')); + } +} + +function printConsoleReport(report) { + console.log(`Detector benchmark ${report.quick ? '(quick)' : '(full)'}`); + console.log(`Cases: ${report.cases.length}`); + const caseRows = report.cases.map(testCase => ({ + status: testCase.status, + engine: testCase.engine, + mode: testCase.mode, + totalMs: testCase.totalMs, + findings: testCase.findings, + target: testCase.target, + })); + printRows(caseRows, [ + { key: 'status', label: 'Status', width: 8 }, + { key: 'engine', label: 'Engine', width: 12 }, + { key: 'mode', label: 'Mode', width: 20 }, + { key: 'totalMs', label: 'Total ms', width: 10 }, + { key: 'findings', label: 'Findings', width: 8 }, + { key: 'target', label: 'Target', width: 60 }, + ]); + + const skipped = report.cases.filter(testCase => testCase.status === 'skipped'); + for (const testCase of skipped) { + console.log(`Skipped ${testCase.name}: ${testCase.skipReason}`); + } + + console.log('\nSlowest profile groups'); + const slowRows = report.summary.slice(0, 20).map(item => ({ + engine: item.engine, + phase: item.phase, + ruleId: item.ruleId, + calls: item.calls, + totalMs: item.totalMs, + avgMs: item.avgMs, + p95: item.p95, + findings: item.findings, + target: item.target, + })); + printRows(slowRows, [ + { key: 'engine', label: 'Engine', width: 12 }, + { key: 'phase', label: 'Phase', width: 14 }, + { key: 'ruleId', label: 'Rule', width: 28 }, + { key: 'calls', label: 'Calls', width: 8 }, + { key: 'totalMs', label: 'Total ms', width: 10 }, + { key: 'avgMs', label: 'Avg ms', width: 8 }, + { key: 'p95', label: 'P95', width: 8 }, + { key: 'findings', label: 'Finds', width: 7 }, + { key: 'target', label: 'Target', width: 45 }, + ]); +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const cases = [ + ...await runFileBenchmarks(args), + ]; + if (args.browser) { + cases.push(...await runBrowserBenchmarks(args)); + } + + const report = makeReport(args, cases); + const json = JSON.stringify(report, null, 2); + if (args.out) { + fs.writeFileSync(path.resolve(args.out), json + '\n'); + } + if (args.json) { + process.stdout.write(json + '\n'); + } else { + printConsoleReport(report); + if (args.out) console.log(`\nWrote JSON report to ${path.resolve(args.out)}`); + } + + if (report.cases.some(testCase => testCase.status === 'failed')) { + process.exitCode = 1; + } +} + +main().catch(err => { + console.error(err?.stack || err?.message || err); + process.exit(1); +}); diff --git a/scripts/build-browser-detector.js b/scripts/build-browser-detector.js index b2ad5ba95111ad6e5e5f9fe5599398264d8a7d41..ef6e8a1e76f8574b116ebb87a708c5deb3fb475b 100644 --- a/scripts/build-browser-detector.js +++ b/scripts/build-browser-detector.js @@ -1,8 +1,8 @@ #!/usr/bin/env node /** - * Generates src/detect-antipatterns-browser.js - * by stripping Node-specific sections from the universal source and wrapping in an IIFE. + * Generates cli/engine/detect-antipatterns-browser.js + * by concatenating the browser-safe detector modules and wrapping them in an IIFE. * * Run: node scripts/build-browser-detector.js */ @@ -14,24 +14,36 @@ import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); -const SOURCE = path.join(ROOT, 'src/detect-antipatterns.mjs'); -const OUTPUT = path.join(ROOT, 'src/detect-antipatterns-browser.js'); - -let code = fs.readFileSync(SOURCE, 'utf-8'); - -// Strip shebang -code = code.replace(/^#!.*\n/, ''); -// Strip sections between @browser-strip-start / @browser-strip-end markers -code = code.replace(/^\/\/ @browser-strip-start\n[\s\S]*?^\/\/ @browser-strip-end\n?/gm, ''); -// Set IS_BROWSER = true (dead-code eliminates Node paths) -code = code.replace(/^const IS_BROWSER = .*$/m, 'const IS_BROWSER = true;'); +const MODULES = [ + 'cli/engine/shared/constants.mjs', + 'cli/engine/registry/antipatterns.mjs', + 'cli/engine/shared/color.mjs', + 'cli/engine/rules/checks.mjs', + 'cli/engine/browser/injected/index.mjs', +]; +const OUTPUT = path.join(ROOT, 'cli/engine/detect-antipatterns-browser.js'); +const SITE_OUTPUT = path.join(ROOT, 'site/public/js/detect-antipatterns-browser.js'); + +function browserSafeModule(relPath) { + let code = fs.readFileSync(path.join(ROOT, relPath), 'utf-8'); + if (relPath === 'cli/engine/registry/antipatterns.mjs') { + const match = code.match(/const ANTIPATTERNS = \[[\s\S]*?\n\];/); + if (!match) throw new Error('Could not extract browser antipattern registry'); + code = match[0]; + } + code = code.replace(/^import[\s\S]*?;\n/gm, ''); + code = code.replace(/^export\s+\{[\s\S]*?^};\n?/gm, ''); + return `// --- ${relPath} ---\n${code.trim()}\n`; +} + +const code = MODULES.map(browserSafeModule).join('\n'); const output = `/** * Anti-Pattern Browser Detector for Impeccable * Copyright (c) 2026 Paul Bakaus * SPDX-License-Identifier: Apache-2.0 * - * GENERATED -- do not edit. Source: detect-antipatterns.mjs + * GENERATED -- do not edit. Source: cli/engine/browser/injected/index.mjs * Rebuild: node scripts/build-browser-detector.js * * Usage: @@ -44,4 +56,7 @@ ${code} `; fs.writeFileSync(OUTPUT, output); +fs.mkdirSync(path.dirname(SITE_OUTPUT), { recursive: true }); +fs.writeFileSync(SITE_OUTPUT, output); console.log(`Generated ${path.relative(ROOT, OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`); +console.log(`Generated ${path.relative(ROOT, SITE_OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`); diff --git a/scripts/build-extension.js b/scripts/build-extension.js index 252fc3fdc456ab32e200678451b93541c126aae3..65d693de8abd846f6243eb07aaaa9181fc5d23c0 100644 --- a/scripts/build-extension.js +++ b/scripts/build-extension.js @@ -13,32 +13,44 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); const EXT_DIR = path.join(ROOT, 'extension'); -const SOURCE = path.join(ROOT, 'src/detect-antipatterns.mjs'); +const BROWSER_MODULES = [ + 'cli/engine/shared/constants.mjs', + 'cli/engine/registry/antipatterns.mjs', + 'cli/engine/shared/color.mjs', + 'cli/engine/rules/checks.mjs', + 'cli/engine/browser/injected/index.mjs', +]; const DETECTOR_OUTPUT = path.join(EXT_DIR, 'detector/detect.js'); const AP_OUTPUT = path.join(EXT_DIR, 'detector/antipatterns.json'); -let code = fs.readFileSync(SOURCE, 'utf-8'); +function browserSafeModule(relPath) { + let code = fs.readFileSync(path.join(ROOT, relPath), 'utf-8'); + if (relPath === 'cli/engine/registry/antipatterns.mjs') { + const match = code.match(/const ANTIPATTERNS = \[[\s\S]*?\n\];/); + if (!match) throw new Error('Could not extract browser antipattern registry'); + code = match[0]; + } + code = code.replace(/^import[\s\S]*?;\n/gm, ''); + code = code.replace(/^export\s+\{[\s\S]*?^};\n?/gm, ''); + return `// --- ${relPath} ---\n${code.trim()}\n`; +} -// --- 1. Build detector --- +const code = BROWSER_MODULES.map(browserSafeModule).join('\n'); -// Strip shebang -code = code.replace(/^#!.*\n/, ''); -// Strip sections between @browser-strip-start / @browser-strip-end markers -code = code.replace(/^\/\/ @browser-strip-start\n[\s\S]*?^\/\/ @browser-strip-end\n?/gm, ''); -// Set IS_BROWSER = true (dead-code eliminates Node paths) -code = code.replace(/^const IS_BROWSER = .*$/m, 'const IS_BROWSER = true;'); +// --- 1. Build detector --- const output = `/** * Anti-Pattern Browser Detector for Impeccable (Extension Variant) * Copyright (c) 2026 Paul Bakaus * SPDX-License-Identifier: Apache-2.0 * - * GENERATED -- do not edit. Source: detect-antipatterns.mjs + * GENERATED -- do not edit. Source: cli/engine/browser/injected/index.mjs * Rebuild: node scripts/build-extension.js */ (function () { @@ -53,22 +65,16 @@ console.log(`Generated ${path.relative(ROOT, DETECTOR_OUTPUT)} (${(output.length // --- 2. Extract antipatterns.json --- -const rawSource = fs.readFileSync(SOURCE, 'utf-8'); -const apMatch = rawSource.match(/const ANTIPATTERNS = \[([\s\S]*?)\n\];/); -if (apMatch) { - // Convert JS object literals to JSON. Include description so the - // devtools panel can show the full rule explanation in tooltips — - // previously this dropped description and the panel had nothing to display. - const antipatterns = new Function(`return [${apMatch[1]}]`)(); - const apJson = antipatterns.map(({ id, name, category, description }) => ({ - id, - name, - category: category || 'quality', - description: description || '', - })); - fs.writeFileSync(AP_OUTPUT, JSON.stringify(apJson, null, 2) + '\n'); - console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${antipatterns.length} rules)`); -} +// Include description so the devtools panel can show the full rule explanation +// in tooltips. +const apJson = ANTIPATTERNS.map(({ id, name, category, description }) => ({ + id, + name, + category: category || 'quality', + description: description || '', +})); +fs.writeFileSync(AP_OUTPUT, JSON.stringify(apJson, null, 2) + '\n'); +console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${ANTIPATTERNS.length} rules)`); // --- 3. Zip packaging --- diff --git a/scripts/build-sub-pages.js b/scripts/build-sub-pages.js deleted file mode 100644 index a8f8b7eae61a2d9738a905ec967ff5445e217368..0000000000000000000000000000000000000000 --- a/scripts/build-sub-pages.js +++ /dev/null @@ -1,735 +0,0 @@ -/** - * Generate static HTML files for /skills, /anti-patterns, /tutorials. - * - * Called from both scripts/build.js (before buildStaticSite) and - * server/index.js (at module load), so dev and prod share the same - * code path and output shape. - * - * Output lives under public/skills/, public/anti-patterns/, - * public/tutorials/, all gitignored. Bun's HTML loader picks them up - * the same way it picks up the hand-authored pages. - */ - -import fs from 'node:fs'; -import path from 'node:path'; -import { - buildSubPageData, - CATEGORY_ORDER, - CATEGORY_LABELS, - CATEGORY_DESCRIPTIONS, - LAYER_LABELS, - LAYER_DESCRIPTIONS, - GALLERY_ITEMS, -} from './lib/sub-pages-data.js'; -import { renderMarkdown, slugify } from './lib/render-markdown.js'; -import { renderPage } from './lib/render-page.js'; - -function escapeHtml(str) { - return String(str || '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -/** - * Render the before/after split-compare demo block for a skill. - * Returns '' when the skill has no demo data (e.g. /shape). - */ -function renderSkillDemo(skill) { - if (!skill.demo) return ''; - const { before, after, caption } = skill.demo; - return ` -
-
-

Drag or hover to compare

-
-
-
${before}
-
-
-
${after || before}
-
-
-
-
- Before - ${caption ? `

${escapeHtml(caption)}

` : ''} - After -
-
-
`; -} - -/** - * Render one skill detail page HTML body (without the site shell). - */ -function renderSkillDetail(skill, knownSkillIds) { - const bodyHtml = renderMarkdown(skill.body, { - knownSkillIds, - currentSkillId: skill.id, - }); - - const editorialHtml = skill.editorial - ? renderMarkdown(skill.editorial.body, { knownSkillIds, currentSkillId: skill.id }) - : ''; - - const demoHtml = renderSkillDemo(skill); - - const tagline = skill.editorial?.frontmatter?.tagline || skill.description; - const categoryLabel = CATEGORY_LABELS[skill.category] || skill.category; - - // Reference files as collapsible
blocks - let referencesHtml = ''; - if (skill.references && skill.references.length > 0) { - const refs = skill.references - .map((ref) => { - const slug = slugify(ref.name); - const refBody = renderMarkdown(ref.content, { - knownSkillIds, - currentSkillId: skill.id, - }); - const title = ref.name - .split('-') - .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) - .join(' '); - return ` -
- Reference${escapeHtml(title)} -
-${refBody} -
-
`; - }) - .join('\n'); - referencesHtml = ` -
-

Deeper reference

- ${refs} -
`; - } - - const metaStrip = ` -
- ${escapeHtml(categoryLabel)} - User-invocable - ${skill.argumentHint ? `${escapeHtml(skill.argumentHint)}` : ''} -
`; - - const hasDemo = demoHtml.trim().length > 0; - - return ` -
-
-
-

Skills / ${escapeHtml(categoryLabel)}

-

/${escapeHtml(skill.id)}

-

${escapeHtml(tagline)}

- ${metaStrip} -
- ${demoHtml} -
- - ${editorialHtml ? `
\n${editorialHtml}\n
` : ''} - -
-
- SKILL.md - The canonical skill definition your AI harness loads. -
-
-${bodyHtml} -
-
- - ${referencesHtml} -
-`; -} - -/** - * Render the unified Docs sidebar used across /skills and /tutorials. - * Shows every skill grouped by category, then tutorials as a final - * group. Pass the current page identifier so we can mark it: - * - * { kind: 'skill', id: 'polish' } - * { kind: 'tutorial', slug: 'getting-started' } - * null (no current page) - */ -function renderDocsSidebar(skillsByCategory, tutorials, current = null) { - // Label the toggle button with the current page so mobile users know - // where they are at a glance, then open the menu to switch. - let currentLabel = 'Docs menu'; - if (current?.kind === 'skill') { - currentLabel = `/${current.id}`; - } else if (current?.kind === 'tutorial') { - const t = tutorials.find((x) => x.slug === current.slug); - if (t) currentLabel = t.title; - } - - let html = ` -`; - return html; -} - -/** - * Render the /skills overview main column content (not the sidebar). - * This is the orientation piece: what skills are, how to pick one, - * the six categories explained with inline cross-links to detail pages. - */ -function renderSkillsOverviewMain(skillsByCategory) { - const totalSkills = Object.values(skillsByCategory).reduce( - (sum, list) => sum + list.length, - 0, - ); - - let categoriesHtml = ''; - for (const category of CATEGORY_ORDER) { - const list = skillsByCategory[category] || []; - if (list.length === 0) continue; - - const skillChips = list - .map( - (s) => - `/${escapeHtml(s.id)}`, - ) - .join(''); - - categoriesHtml += ` -
-
-

${escapeHtml(CATEGORY_LABELS[category])}

-

${list.length} ${list.length === 1 ? 'skill' : 'skills'}

-
-

${escapeHtml(CATEGORY_DESCRIPTIONS[category])}

-
-${skillChips} -
-
-`; - } - - return ` -
-
-

${totalSkills} commands

-

Skills

-

One skill, /impeccable, teaches your AI design. Eighteen commands steer the result. Each command does one job with an opinion about what good looks like.

-
- -
-

How to pick one

-

Skills are named after the intent you bring to them. Reviewing something? /critique or /audit. Fixing type? /typeset. Last-mile pass before shipping? /polish. The categories below group skills by the job.

-
- -
-${categoriesHtml} -
-
`; -} - -/** - * Wrap sidebar + main content in the docs-browser layout shell. - */ -function wrapInDocsLayout(sidebarHtml, mainHtml) { - return ` -
- ${sidebarHtml} -
-${mainHtml} -
-
`; -} - -/** - * Group anti-pattern rules by skill section. - * Rules without a skillSection fall into a 'General quality' bucket. - */ -function groupRulesBySection(rules) { - // Canonical ordering. Additional sections referenced by rules (e.g. - // 'Interaction', 'Responsive' from LLM-only entries) are appended to - // the end, before 'General quality', so every rule renders. - const primaryOrder = [ - 'Visual Details', - 'Typography', - 'Color & Contrast', - 'Layout & Space', - 'Motion', - 'Interaction', - 'Responsive', - ]; - const bySection = {}; - for (const name of primaryOrder) bySection[name] = []; - bySection['General quality'] = []; - - for (const rule of rules) { - const section = rule.skillSection || 'General quality'; - if (!bySection[section]) bySection[section] = []; - bySection[section].push(rule); - } - - // Sort each bucket: slop first (they're the named tells), then quality. - for (const name of Object.keys(bySection)) { - bySection[name].sort((a, b) => { - if (a.category !== b.category) return a.category === 'slop' ? -1 : 1; - return a.name.localeCompare(b.name); - }); - } - - // Final render order: primary sections first, then any extras that - // rules introduced, then General quality last. - const order = [...primaryOrder]; - for (const name of Object.keys(bySection)) { - if (!order.includes(name) && name !== 'General quality') { - order.push(name); - } - } - order.push('General quality'); - - return { order, bySection }; -} - -/** - * Render the anti-patterns sidebar: a table of contents of rule sections - * with per-section rule counts. Every entry anchor-jumps to the section - * in the main column. - */ -function renderAntiPatternsSidebar(grouped) { - const entries = grouped.order - .filter((section) => grouped.bySection[section]?.length > 0) - .map((section) => { - const slug = slugify(section); - const count = grouped.bySection[section].length; - return `
  • ${escapeHtml(section)}${count}
  • `; - }) - .join('\n'); - - return ` -`; -} - -/** - * Render one rule card inside the anti-patterns main column. - */ -function renderRuleCard(rule) { - const categoryLabel = rule.category === 'slop' ? 'AI slop' : 'Quality'; - const layer = rule.layer || 'cli'; - const layerLabel = LAYER_LABELS[layer] || layer; - const layerTitle = LAYER_DESCRIPTIONS[layer] || ''; - const skillLink = rule.skillSection - ? `See in /impeccable` - : ''; - const visual = rule.visual - ? `` - : ''; - return ` -
    - ${visual} -
    -
    - ${categoryLabel} - ${escapeHtml(layerLabel)} -
    -

    ${escapeHtml(rule.name)}

    -

    ${escapeHtml(rule.description)}

    - ${skillLink} -
    -
    `; -} - -function escapeAttr(str) { - return String(str || '').replace(/"/g, '"'); -} - -/** - * Render the /tutorials index main content. - */ -function renderTutorialsIndexMain(tutorials) { - const cards = tutorials - .map( - (t) => ` - - ${String(t.order).padStart(2, '0')} -
    -

    ${escapeHtml(t.title)}

    -

    ${escapeHtml(t.tagline || t.description)}

    -
    - -
    `, - ) - .join('\n'); - - return ` -
    -
    -

    ${tutorials.length} walk-throughs

    -

    Tutorials

    -

    Short, opinionated walk-throughs of the highest-leverage workflows. Each one takes around ten minutes and ends with something working in your project.

    -
    - -
    -${cards} -
    -
    `; -} - -/** - * Render the /visual-mode page main content. - * - * Single-column layout, no sidebar. Editorial header, live iframe embed - * of the detector running on a synthetic slop page, three-card section - * explaining the invocation methods, then a grid of real specimens the - * user can click into to see the overlay on a different page. - */ -function renderVisualModeMain() { - const specimenCards = GALLERY_ITEMS.map( - (item) => ` - - - - `, - ).join('\n'); - - return ` -
    -
    -

    Live detection overlay

    -

    Visual Mode

    -

    See every anti-pattern flagged directly on the page. No screenshots, no JSON to map back to line numbers. The overlay draws an outline and a label on every element the detector catches, so you fix them in place.

    -
    - -
    -
    -
    - - - - Live on a synthetic slop page -
    - -
    -

    Hover or tap any outlined element to see which rule fired.

    -
    - -
    -

    Three ways to run it

    -
    -
    -

    Inside /critique

    -

    /critique

    -

    The design review skill opens the overlay automatically during its browser assessment pass. You get the deterministic findings highlighted in place while the LLM runs its separate heuristic review.

    -
    -
    -

    Standalone CLI

    -

    npx impeccable live

    -

    Starts a local server that serves the detector script. Inject it into any page via a <script> tag to see the overlay. Works on your own dev server, a staging URL, or anyone's live page.

    -
    - -
    -
    - - -
    `; -} - -/** - * Render a tutorial detail page main content. - */ -function renderTutorialDetail(tutorial, knownSkillIds) { - const bodyHtml = renderMarkdown(tutorial.body, { knownSkillIds }); - return ` -
    -
    -

    Tutorials / ${String(tutorial.order).padStart(2, '0')}

    -

    ${escapeHtml(tutorial.title)}

    - ${tutorial.tagline ? `

    ${escapeHtml(tutorial.tagline)}

    ` : ''} -
    - -
    -${bodyHtml} -
    -
    `; -} - -/** - * Render the /anti-patterns main column content. - */ -function renderAntiPatternsMain(grouped, totalRules) { - let sectionsHtml = ''; - for (const section of grouped.order) { - const rules = grouped.bySection[section] || []; - if (rules.length === 0) continue; - const slug = slugify(section); - sectionsHtml += ` -
    -
    -

    ${escapeHtml(section)}

    -

    ${rules.length} ${rules.length === 1 ? 'rule' : 'rules'}

    -
    -
    -${rules.map(renderRuleCard).join('\n')} -
    -
    `; - } - - const detectedCount = grouped.order - .flatMap((s) => grouped.bySection[s] || []) - .filter((r) => r.layer !== 'llm').length; - const llmCount = totalRules - detectedCount; - - return ` -
    -
    -

    ${totalRules} rules

    -

    Anti-patterns

    -

    The full catalog of patterns /impeccable teaches against. ${detectedCount} are caught by a deterministic detector (npx impeccable detect or the browser extension). ${llmCount} can only be flagged by /critique's LLM review pass. Want to see them live on real pages? Try Visual Mode.

    -
    - -
    - - How to read this - - -
    -

    AI slop rules flag the visible tells of AI-generated UIs. Quality rules flag general design mistakes that are not AI-specific but still hurt the work. Each rule also shows how it is detected:

    -
    -
    CLI
    Deterministic. Runs from npx impeccable detect on files, no browser required.
    -
    Browser
    Deterministic, but needs real browser layout. Runs via the browser extension or Puppeteer, not the plain CLI.
    -
    LLM only
    No deterministic detector. Caught by /critique during its LLM design review.
    -
    -
    -
    - -
    -${sectionsHtml} -
    -
    `; -} - -/** - * Entry point. Generates all sub-page HTML files. - * - * @param {string} rootDir - * @returns {Promise<{ files: string[] }>} list of generated file paths (absolute) - */ -export async function generateSubPages(rootDir) { - const data = await buildSubPageData(rootDir); - const outDirs = { - skills: path.join(rootDir, 'public/skills'), - antiPatterns: path.join(rootDir, 'public/anti-patterns'), - tutorials: path.join(rootDir, 'public/tutorials'), - visualMode: path.join(rootDir, 'public/visual-mode'), - }; - - // Fresh output dirs each time so stale files don't linger. - for (const dir of Object.values(outDirs)) { - if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true }); - fs.mkdirSync(dir, { recursive: true }); - } - - const generated = []; - - // Skills index: docs-browser layout with unified sidebar. - { - const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, null); - const main = renderSkillsOverviewMain(data.skillsByCategory); - const html = renderPage({ - title: 'Skills | Impeccable', - description: - '18 commands that teach your AI harness how to design. Browse by category: create, evaluate, refine, simplify, harden.', - bodyHtml: wrapInDocsLayout(sidebar, main), - activeNav: 'docs', - canonicalPath: '/skills', - bodyClass: 'sub-page skills-layout-page', - }); - const out = path.join(outDirs.skills, 'index.html'); - fs.writeFileSync(out, html, 'utf-8'); - generated.push(out); - } - - // Skills detail pages: same docs-browser shell as the overview. - for (const skill of data.skills) { - const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, { kind: 'skill', id: skill.id }); - const main = renderSkillDetail(skill, data.knownSkillIds); - const title = `/${skill.id} | Impeccable`; - const description = skill.editorial?.frontmatter?.tagline || skill.description; - const html = renderPage({ - title, - description, - bodyHtml: wrapInDocsLayout(sidebar, main), - activeNav: 'docs', - canonicalPath: `/skills/${skill.id}`, - bodyClass: 'sub-page skills-layout-page', - }); - const out = path.join(outDirs.skills, `${skill.id}.html`); - fs.writeFileSync(out, html, 'utf-8'); - generated.push(out); - } - - // Anti-patterns index: single page, docs-browser shell with TOC sidebar. - { - const grouped = groupRulesBySection(data.rules); - const sidebar = renderAntiPatternsSidebar(grouped); - const main = renderAntiPatternsMain(grouped, data.rules.length); - const html = renderPage({ - title: 'Anti-patterns | Impeccable', - description: `${data.rules.length} deterministic detection rules that flag the visible tells of AI-generated interfaces and common quality issues. Used by npx impeccable detect and the browser extension.`, - bodyHtml: wrapInDocsLayout(sidebar, main), - activeNav: 'anti-patterns', - canonicalPath: '/anti-patterns', - bodyClass: 'sub-page skills-layout-page anti-patterns-page', - }); - const out = path.join(outDirs.antiPatterns, 'index.html'); - fs.writeFileSync(out, html, 'utf-8'); - generated.push(out); - } - - // Tutorials index (under the unified Docs umbrella). - if (data.tutorials.length > 0) { - const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, null); - const main = renderTutorialsIndexMain(data.tutorials); - const html = renderPage({ - title: 'Tutorials | Impeccable', - description: `${data.tutorials.length} short, opinionated walk-throughs of the highest-leverage Impeccable workflows.`, - bodyHtml: wrapInDocsLayout(sidebar, main), - activeNav: 'docs', - canonicalPath: '/tutorials', - bodyClass: 'sub-page skills-layout-page tutorials-page', - }); - const out = path.join(outDirs.tutorials, 'index.html'); - fs.writeFileSync(out, html, 'utf-8'); - generated.push(out); - } - - // Visual Mode: single standalone page, no sidebar, single-column layout. - { - const html = renderPage({ - title: 'Visual Mode | Impeccable', - description: - 'See every anti-pattern flagged directly on the page. Live detection overlay from Impeccable, available via /critique, npx impeccable live, or the upcoming Chrome extension.', - bodyHtml: renderVisualModeMain(), - activeNav: 'visual-mode', - canonicalPath: '/visual-mode', - bodyClass: 'sub-page visual-mode-page-body', - }); - const out = path.join(outDirs.visualMode, 'index.html'); - fs.writeFileSync(out, html, 'utf-8'); - generated.push(out); - } - - // Tutorial detail pages. - for (const tutorial of data.tutorials) { - const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, { kind: 'tutorial', slug: tutorial.slug }); - const main = renderTutorialDetail(tutorial, data.knownSkillIds); - const html = renderPage({ - title: `${tutorial.title} | Tutorials | Impeccable`, - description: tutorial.description || tutorial.tagline || '', - bodyHtml: wrapInDocsLayout(sidebar, main), - activeNav: 'docs', - canonicalPath: `/tutorials/${tutorial.slug}`, - bodyClass: 'sub-page skills-layout-page tutorials-page', - }); - const out = path.join(outDirs.tutorials, `${tutorial.slug}.html`); - fs.writeFileSync(out, html, 'utf-8'); - generated.push(out); - } - - return { files: generated }; -} diff --git a/scripts/build.js b/scripts/build.js index 4668d3cd4f356074647446e4f248c47eacbcf681..4934ed03d4082a2f0f8878a43f1ccabffc846d1d 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -7,8 +7,9 @@ * - Cursor: .cursor/skills/ * - Claude Code: .claude/skills/ * - Gemini: .gemini/skills/ - * - Codex: .codex/skills/ - * - Agents: .agents/skills/ (VS Code Copilot + Antigravity) + * - Codex: dist/codex/ only (OpenAI-metadata bundle; not synced to repo root) + * - Agents: .agents/skills/ (Codex repo/user installs) + * - GitHub: .github/skills/ (GitHub Copilot) * * Also assembles a universal ZIP containing all providers, * and builds Tailwind CSS for production deployment. @@ -17,35 +18,42 @@ import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; -import { readSourceFiles, readPatterns } from './lib/utils.js'; +import { readSourceFiles, readPatterns, stashPerProjectArtifacts, restorePerProjectArtifacts } from './lib/utils.js'; import { createTransformer, PROVIDERS } from './lib/transformers/index.js'; import { createAllZips } from './lib/zip.js'; -import { generateSubPages } from './build-sub-pages.js'; +import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs'; +// Sub-page generation is now handled by Astro content collections. /** - * Generate authoritative counts from source data and write to public/js/generated/counts.js. + * Generate authoritative counts from source data and write to site/public/js/generated/counts.js. * Also validates that key HTML files reference the correct numbers. */ function generateCounts(rootDir, skills, buildDir) { - // Count active (non-deprecated) user-invocable commands - const activeCommands = skills.filter(s => { - if (!s.userInvocable) return false; - const content = fs.readFileSync(s.filePath, 'utf-8'); - return !content.includes('DEPRECATED'); - }); - const commandCount = activeCommands.length; - - // Count detection rules from impeccable package - const detectPkgPath = path.join(rootDir, 'src/detect-antipatterns.mjs'); - const detectorSrc = fs.readFileSync(detectPkgPath, 'utf-8'); - const ruleIds = new Set(); - for (const match of detectorSrc.matchAll(/^\s+id: '([^']+)'/gm)) { - ruleIds.add(match[1]); + // Count active commands. After the v3.0 consolidation, commands are sub-commands + // of /impeccable. Count them from the command router table in SKILL.md. + const impeccableSkill = skills.find(s => s.name === 'impeccable'); + let commandCount; + if (impeccableSkill) { + // Count lines in the command table that start with | `...` | — tolerant + // of argument hints inside the backticks (e.g. `craft [feature]`) and of + // multi-word commands (e.g. `pin `). + const routerMatches = impeccableSkill.body.match(/^\| `[^`]+` \|/gm); + commandCount = routerMatches ? routerMatches.length : 0; + } else { + // Fallback: count user-invocable skills + const activeCommands = skills.filter(s => { + if (!s.userInvocable) return false; + const content = fs.readFileSync(s.filePath, 'utf-8'); + return !content.includes('DEPRECATED'); + }); + commandCount = activeCommands.length; } - const detectionCount = ruleIds.size; + + // Count detection rules from the detector registry. + const detectionCount = new Set(ANTIPATTERNS.map(rule => rule.id)).size; // Write generated counts module - const genDir = path.join(rootDir, 'public/js/generated'); + const genDir = path.join(rootDir, 'site/public/js/generated'); fs.mkdirSync(genDir, { recursive: true }); fs.writeFileSync(path.join(genDir, 'counts.js'), `// GENERATED by build.js — do not edit\n` + @@ -55,8 +63,7 @@ function generateCounts(rootDir, skills, buildDir) { // Validate counts in key files const filesToCheck = [ - 'public/index.html', - 'public/cheatsheet.html', + 'site/pages/index.astro', 'README.md', 'NOTICE.md', 'AGENTS.md', @@ -73,7 +80,7 @@ function generateCounts(rootDir, skills, buildDir) { // Check for stale command counts (look for "N commands" or "N skills" patterns) // Strip changelog list content to avoid flagging historical counts const strippedContent = content.replace(/