feat: add web frontend rules and design quality hook

This commit is contained in:
Affaan Mustafa
2026-04-02 17:33:17 -07:00
parent a60d5fbc00
commit 31c9f7c33e
15 changed files with 772 additions and 2 deletions

View File

@@ -37,7 +37,7 @@
{
"command": "node .cursor/hooks/after-file-edit.js",
"event": "afterFileEdit",
"description": "Auto-format, TypeScript check, console.log warning"
"description": "Auto-format, TypeScript check, console.log warning, and frontend design-quality reminder"
}
],
"beforeMCPExecution": [

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node
const { readStdin, runExistingHook, transformToClaude } = require('./adapter');
const { hookEnabled, readStdin, runExistingHook, transformToClaude } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
@@ -11,6 +11,9 @@ readStdin().then(raw => {
// Accumulate edited paths for batch format+typecheck at stop time
runExistingHook('post-edit-accumulator.js', claudeStr);
runExistingHook('post-edit-console-warn.js', claudeStr);
if (hookEnabled('post:edit:design-quality-check', ['standard', 'strict'])) {
runExistingHook('design-quality-check.js', claudeStr);
}
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -115,3 +115,5 @@ Keep this file detailed for only the current sprint, blockers, and next actions.
- 2026-04-02: Closed fresh auto-generated bundle PRs `#1182` and `#1183` under the existing policy. Useful ideas from generator output must be ported manually into canonical repo surfaces instead of merging `.claude`/bundle PRs wholesale.
- 2026-04-02: Ported the safe one-file macOS observer fix from `#1164` directly into `main` as a POSIX `mkdir` fallback for `continuous-learning-v2` lazy-start locking, then closed the PR as superseded by direct port.
- 2026-04-02: Ported the safe core of `#1153` directly into `main`: markdownlint cleanup for orchestration/docs surfaces plus the Windows `USERPROFILE` and path-normalization fixes in `install-apply` / `repair` tests. Local validation after installing repo deps: `node tests/scripts/install-apply.test.js`, `node tests/scripts/repair.test.js`, and targeted `yarn markdownlint` all passed.
- 2026-04-02: Direct-ported the safe web/frontend rules lane from `#1122` into `rules/web/`, but adapted `rules/web/hooks.md` to prefer project-local tooling and avoid remote one-off package execution examples.
- 2026-04-02: Adapted the design-quality reminder from `#1127` into the current ECC hook architecture with a local `scripts/hooks/design-quality-check.js`, Claude `hooks/hooks.json` wiring, Cursor `after-file-edit.js` wiring, and dedicated hook coverage in `tests/hooks/design-quality-check.test.js`.

View File

@@ -35,6 +35,7 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes
| **PR logger** | `Bash` | Logs PR URL and review command after `gh pr create` |
| **Build analysis** | `Bash` | Background analysis after build commands (async, non-blocking) |
| **Quality gate** | `Edit\|Write\|MultiEdit` | Runs fast quality checks after edits |
| **Design quality check** | `Edit\|Write\|MultiEdit` | Warns when frontend edits drift toward generic template-looking UI |
| **Prettier format** | `Edit` | Auto-formats JS/TS files with Prettier after edits |
| **TypeScript check** | `Edit` | Runs `tsc --noEmit` after editing `.ts`/`.tsx` files |
| **console.log warning** | `Edit` | Warns about `console.log` statements in edited files |

View File

@@ -226,6 +226,18 @@
"description": "Run quality gate checks after file edits",
"id": "post:quality-gate"
},
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"post:edit:design-quality-check\" \"scripts/hooks/design-quality-check.js\" \"standard,strict\"",
"timeout": 10
}
],
"description": "Warn when frontend edits drift toward generic template-looking UI",
"id": "post:edit:design-quality-check"
},
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [

View File

@@ -17,6 +17,7 @@ rules/
├── typescript/ # TypeScript/JavaScript specific
├── python/ # Python specific
├── golang/ # Go specific
├── web/ # Web and frontend specific
├── swift/ # Swift specific
└── php/ # PHP specific
```
@@ -33,6 +34,7 @@ rules/
./install.sh typescript
./install.sh python
./install.sh golang
./install.sh web
./install.sh swift
./install.sh php
@@ -56,6 +58,7 @@ cp -r rules/common ~/.claude/rules/common
cp -r rules/typescript ~/.claude/rules/typescript
cp -r rules/python ~/.claude/rules/python
cp -r rules/golang ~/.claude/rules/golang
cp -r rules/web ~/.claude/rules/web
cp -r rules/swift ~/.claude/rules/swift
cp -r rules/php ~/.claude/rules/php
@@ -86,6 +89,8 @@ To add support for a new language (e.g., `rust/`):
```
4. Reference existing skills if available, or create new ones under `skills/`.
For non-language domains like `web/`, follow the same layered pattern when there is enough reusable domain-specific guidance to justify a standalone ruleset.
## Rule Priority
When language-specific rules and common rules conflict, **language-specific rules take precedence** (specific overrides general). This follows the standard layered configuration pattern (similar to CSS specificity or `.gitignore` precedence).

96
rules/web/coding-style.md Normal file
View File

@@ -0,0 +1,96 @@
> This file extends [common/coding-style.md](../common/coding-style.md) with web-specific frontend content.
# Web Coding Style
## File Organization
Organize by feature or surface area, not by file type:
```text
src/
├── components/
│ ├── hero/
│ │ ├── Hero.tsx
│ │ ├── HeroVisual.tsx
│ │ └── hero.css
│ ├── scrolly-section/
│ │ ├── ScrollySection.tsx
│ │ ├── StickyVisual.tsx
│ │ └── scrolly.css
│ └── ui/
│ ├── Button.tsx
│ ├── SurfaceCard.tsx
│ └── AnimatedText.tsx
├── hooks/
│ ├── useReducedMotion.ts
│ └── useScrollProgress.ts
├── lib/
│ ├── animation.ts
│ └── color.ts
└── styles/
├── tokens.css
├── typography.css
└── global.css
```
## CSS Custom Properties
Define design tokens as variables. Do not hardcode palette, typography, or spacing repeatedly:
```css
:root {
--color-surface: oklch(98% 0 0);
--color-text: oklch(18% 0 0);
--color-accent: oklch(68% 0.21 250);
--text-base: clamp(1rem, 0.92rem + 0.4vw, 1.125rem);
--text-hero: clamp(3rem, 1rem + 7vw, 8rem);
--space-section: clamp(4rem, 3rem + 5vw, 10rem);
--duration-fast: 150ms;
--duration-normal: 300ms;
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}
```
## Animation-Only Properties
Prefer compositor-friendly motion:
- `transform`
- `opacity`
- `clip-path`
- `filter` (sparingly)
Avoid animating layout-bound properties:
- `width`
- `height`
- `top`
- `left`
- `margin`
- `padding`
- `border`
- `font-size`
## Semantic HTML First
```html
<header>
<nav aria-label="Main navigation">...</nav>
</header>
<main>
<section aria-labelledby="hero-heading">
<h1 id="hero-heading">...</h1>
</section>
</main>
<footer>...</footer>
```
Do not reach for generic wrapper `div` stacks when a semantic element exists.
## Naming
- Components: PascalCase (`ScrollySection`, `SurfaceCard`)
- Hooks: `use` prefix (`useReducedMotion`)
- CSS classes: kebab-case or utility classes
- Animation timelines: camelCase with intent (`heroRevealTl`)

View File

@@ -0,0 +1,63 @@
> This file extends [common/patterns.md](../common/patterns.md) with web-specific design-quality guidance.
# Web Design Quality Standards
## Anti-Template Policy
Do not ship generic template-looking UI. Frontend output should look intentional, opinionated, and specific to the product.
### Banned Patterns
- Default card grids with uniform spacing and no hierarchy
- Stock hero section with centered headline, gradient blob, and generic CTA
- Unmodified library defaults passed off as finished design
- Flat layouts with no layering, depth, or motion
- Uniform radius, spacing, and shadows across every component
- Safe gray-on-white styling with one decorative accent color
- Dashboard-by-numbers layouts with sidebar + cards + charts and no point of view
- Default font stacks used without a deliberate reason
### Required Qualities
Every meaningful frontend surface should demonstrate at least four of these:
1. Clear hierarchy through scale contrast
2. Intentional rhythm in spacing, not uniform padding everywhere
3. Depth or layering through overlap, shadows, surfaces, or motion
4. Typography with character and a real pairing strategy
5. Color used semantically, not just decoratively
6. Hover, focus, and active states that feel designed
7. Grid-breaking editorial or bento composition where appropriate
8. Texture, grain, or atmosphere when it fits the visual direction
9. Motion that clarifies flow instead of distracting from it
10. Data visualization treated as part of the design system, not an afterthought
## Before Writing Frontend Code
1. Pick a specific style direction. Avoid vague defaults like "clean minimal".
2. Define a palette intentionally.
3. Choose typography deliberately.
4. Gather at least a small set of real references.
5. Use ECC design/frontend skills where relevant.
## Worthwhile Style Directions
- Editorial / magazine
- Neo-brutalism
- Glassmorphism with real depth
- Dark luxury or light luxury with disciplined contrast
- Bento layouts
- Scrollytelling
- 3D integration
- Swiss / International
- Retro-futurism
Do not default to dark mode automatically. Choose the visual direction the product actually wants.
## Component Checklist
- [ ] Does it avoid looking like a default Tailwind or shadcn template?
- [ ] Does it have intentional hover/focus/active states?
- [ ] Does it use hierarchy rather than uniform emphasis?
- [ ] Would this look believable in a real product screenshot?
- [ ] If it supports both themes, do both light and dark feel intentional?

120
rules/web/hooks.md Normal file
View File

@@ -0,0 +1,120 @@
> This file extends [common/hooks.md](../common/hooks.md) with web-specific hook recommendations.
# Web Hooks
## Recommended PostToolUse Hooks
Prefer project-local tooling. Do not wire hooks to remote one-off package execution.
### Format on Save
Use the project's existing formatter entrypoint after edits:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "pnpm prettier --write \"$FILE_PATH\"",
"description": "Format edited frontend files"
}
]
}
}
```
Equivalent local commands via `yarn prettier` or `npm exec prettier --` are fine when they use repo-owned dependencies.
### Lint Check
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "pnpm eslint --fix \"$FILE_PATH\"",
"description": "Run ESLint on edited frontend files"
}
]
}
}
```
### Type Check
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "pnpm tsc --noEmit --pretty false",
"description": "Type-check after frontend edits"
}
]
}
}
```
### CSS Lint
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "pnpm stylelint --fix \"$FILE_PATH\"",
"description": "Lint edited stylesheets"
}
]
}
}
```
## PreToolUse Hooks
### Guard File Size
Block oversized writes from tool input content, not from a file that may not exist yet:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const c=i.tool_input?.content||'';const lines=c.split('\\n').length;if(lines>800){console.error('[Hook] BLOCKED: File exceeds 800 lines ('+lines+' lines)');console.error('[Hook] Split into smaller modules');process.exit(2)}console.log(d)})\"",
"description": "Block writes that exceed 800 lines"
}
]
}
}
```
## Stop Hooks
### Final Build Verification
```json
{
"hooks": {
"Stop": [
{
"command": "pnpm build",
"description": "Verify the production build at session end"
}
]
}
}
```
## Ordering
Recommended order:
1. format
2. lint
3. type check
4. build verification

79
rules/web/patterns.md Normal file
View File

@@ -0,0 +1,79 @@
> This file extends [common/patterns.md](../common/patterns.md) with web-specific patterns.
# Web Patterns
## Component Composition
### Compound Components
Use compound components when related UI shares state and interaction semantics:
```tsx
<Tabs defaultValue="overview">
<Tabs.List>
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="overview">...</Tabs.Content>
<Tabs.Content value="settings">...</Tabs.Content>
</Tabs>
```
- Parent owns state
- Children consume via context
- Prefer this over prop drilling for complex widgets
### Render Props / Slots
- Use render props or slot patterns when behavior is shared but markup must vary
- Keep keyboard handling, ARIA, and focus logic in the headless layer
### Container / Presentational Split
- Container components own data loading and side effects
- Presentational components receive props and render UI
- Presentational components should stay pure
## State Management
Treat these separately:
| Concern | Tooling |
|---------|---------|
| Server state | TanStack Query, SWR, tRPC |
| Client state | Zustand, Jotai, signals |
| URL state | search params, route segments |
| Form state | React Hook Form or equivalent |
- Do not duplicate server state into client stores
- Derive values instead of storing redundant computed state
## URL As State
Persist shareable state in the URL:
- filters
- sort order
- pagination
- active tab
- search query
## Data Fetching
### Stale-While-Revalidate
- Return cached data immediately
- Revalidate in the background
- Prefer existing libraries instead of rolling this by hand
### Optimistic Updates
- Snapshot current state
- Apply optimistic update
- Roll back on failure
- Emit visible error feedback when rolling back
### Parallel Loading
- Fetch independent data in parallel
- Avoid parent-child request waterfalls
- Prefetch likely next routes or states when justified

64
rules/web/performance.md Normal file
View File

@@ -0,0 +1,64 @@
> This file extends [common/performance.md](../common/performance.md) with web-specific performance content.
# Web Performance Rules
## Core Web Vitals Targets
| Metric | Target |
|--------|--------|
| LCP | < 2.5s |
| INP | < 200ms |
| CLS | < 0.1 |
| FCP | < 1.5s |
| TBT | < 200ms |
## Bundle Budget
| Page Type | JS Budget (gzipped) | CSS Budget |
|-----------|---------------------|------------|
| Landing page | < 150kb | < 30kb |
| App page | < 300kb | < 50kb |
| Microsite | < 80kb | < 15kb |
## Loading Strategy
1. Inline critical above-the-fold CSS where justified
2. Preload the hero image and primary font only
3. Defer non-critical CSS or JS
4. Dynamically import heavy libraries
```js
const gsapModule = await import('gsap');
const { ScrollTrigger } = await import('gsap/ScrollTrigger');
```
## Image Optimization
- Explicit `width` and `height`
- `loading="eager"` plus `fetchpriority="high"` for hero media only
- `loading="lazy"` for below-the-fold assets
- Prefer AVIF or WebP with fallbacks
- Never ship source images far beyond rendered size
## Font Loading
- Max two font families unless there is a clear exception
- `font-display: swap`
- Subset where possible
- Preload only the truly critical weight/style
## Animation Performance
- Animate compositor-friendly properties only
- Use `will-change` narrowly and remove it when done
- Prefer CSS for simple transitions
- Use `requestAnimationFrame` or established animation libraries for JS motion
- Avoid scroll handler churn; use IntersectionObserver or well-behaved libraries
## Performance Checklist
- [ ] All images have explicit dimensions
- [ ] No accidental render-blocking resources
- [ ] No layout shifts from dynamic content
- [ ] Motion stays on compositor-friendly properties
- [ ] Third-party scripts load async/defer and only when needed

57
rules/web/security.md Normal file
View File

@@ -0,0 +1,57 @@
> This file extends [common/security.md](../common/security.md) with web-specific security content.
# Web Security Rules
## Content Security Policy
Always configure a production CSP.
### Nonce-Based CSP
Use a per-request nonce for scripts instead of `'unsafe-inline'`.
```text
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://*.example.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
```
Adjust origins to the project. Do not cargo-cult this block unchanged.
## XSS Prevention
- Never inject unsanitized HTML
- Avoid `innerHTML` / `dangerouslySetInnerHTML` unless sanitized first
- Escape dynamic template values
- Sanitize user HTML with a vetted local sanitizer when absolutely necessary
## Third-Party Scripts
- Load asynchronously
- Use SRI when serving from a CDN
- Audit quarterly
- Prefer self-hosting for critical dependencies when practical
## HTTPS and Headers
```text
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
```
## Forms
- CSRF protection on state-changing forms
- Rate limiting on submission endpoints
- Validate client and server side
- Prefer honeypots or light anti-abuse controls over heavy-handed CAPTCHA defaults

55
rules/web/testing.md Normal file
View File

@@ -0,0 +1,55 @@
> This file extends [common/testing.md](../common/testing.md) with web-specific testing content.
# Web Testing Rules
## Priority Order
### 1. Visual Regression
- Screenshot key breakpoints: 320, 768, 1024, 1440
- Test hero sections, scrollytelling sections, and meaningful states
- Use Playwright screenshots for visual-heavy work
- If both themes exist, test both
### 2. Accessibility
- Run automated accessibility checks
- Test keyboard navigation
- Verify reduced-motion behavior
- Verify color contrast
### 3. Performance
- Run Lighthouse or equivalent against meaningful pages
- Keep CWV targets from [performance.md](performance.md)
### 4. Cross-Browser
- Minimum: Chrome, Firefox, Safari
- Test scrolling, motion, and fallback behavior
### 5. Responsive
- Test 320, 375, 768, 1024, 1440, 1920
- Verify no overflow
- Verify touch interactions
## E2E Shape
```ts
import { test, expect } from '@playwright/test';
test('landing hero loads', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
```
- Avoid flaky timeout-based assertions
- Prefer deterministic waits
## Unit Tests
- Test utilities, data transforms, and custom hooks
- For highly visual components, visual regression often carries more signal than brittle markup assertions
- Visual regression supplements coverage targets; it does not replace them

View File

@@ -0,0 +1,131 @@
#!/usr/bin/env node
/**
* PostToolUse hook: lightweight frontend design-quality reminder.
*
* This stays self-contained inside ECC. It does not call remote models or
* install packages. The goal is to catch obviously generic UI drift and keep
* frontend edits aligned with ECC's stronger design standards.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const FRONTEND_EXTENSIONS = /\.(astro|css|html|jsx|scss|svelte|tsx|vue)$/i;
const MAX_STDIN = 1024 * 1024;
const GENERIC_SIGNALS = [
{ pattern: /\bget started\b/i, label: '"Get Started" CTA copy' },
{ pattern: /\blearn more\b/i, label: '"Learn more" CTA copy' },
{ pattern: /\bgrid-cols-(3|4)\b/, label: 'uniform multi-card grid' },
{ pattern: /\bbg-gradient-to-[trbl]/, label: 'stock gradient utility usage' },
{ pattern: /\btext-center\b/, label: 'centered default layout cues' },
{ pattern: /\bfont-(sans|inter)\b/i, label: 'default font utility' },
];
const CHECKLIST = [
'visual hierarchy with real contrast',
'intentional spacing rhythm',
'depth, layering, or overlap',
'purposeful hover and focus states',
'color and typography that feel specific',
];
function getFilePaths(input) {
const toolInput = input?.tool_input || {};
if (toolInput.file_path) {
return [String(toolInput.file_path)];
}
if (Array.isArray(toolInput.edits)) {
return toolInput.edits
.map(edit => String(edit?.file_path || ''))
.filter(Boolean);
}
return [];
}
function readContent(filePath) {
try {
return fs.readFileSync(path.resolve(filePath), 'utf8');
} catch {
return '';
}
}
function detectSignals(content) {
return GENERIC_SIGNALS.filter(signal => signal.pattern.test(content)).map(signal => signal.label);
}
function buildWarning(frontendPaths, findings) {
const pathLines = frontendPaths.map(fp => ` - ${fp}`).join('\n');
const signalLines = findings.length > 0
? findings.map(item => ` - ${item}`).join('\n')
: ' - no obvious canned-template strings detected';
return [
'[Hook] DESIGN CHECK: frontend file(s) modified:',
pathLines,
'[Hook] Review for generic/template drift. Frontend should have:',
CHECKLIST.map(item => ` - ${item}`).join('\n'),
'[Hook] Heuristic signals:',
signalLines,
].join('\n');
}
function run(inputOrRaw) {
let input;
let rawInput = inputOrRaw;
try {
if (typeof inputOrRaw === 'string') {
rawInput = inputOrRaw;
input = inputOrRaw.trim() ? JSON.parse(inputOrRaw) : {};
} else {
input = inputOrRaw || {};
rawInput = JSON.stringify(inputOrRaw ?? {});
}
} catch {
return { exitCode: 0, stdout: typeof rawInput === 'string' ? rawInput : '' };
}
const filePaths = getFilePaths(input);
const frontendPaths = filePaths.filter(filePath => FRONTEND_EXTENSIONS.test(filePath));
if (frontendPaths.length === 0) {
return { exitCode: 0, stdout: typeof rawInput === 'string' ? rawInput : '' };
}
const findings = [];
for (const filePath of frontendPaths) {
const content = readContent(filePath);
findings.push(...detectSignals(content));
}
return {
exitCode: 0,
stdout: typeof rawInput === 'string' ? rawInput : '',
stderr: buildWarning(frontendPaths, findings),
};
}
module.exports = { run };
if (require.main === module) {
let raw = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (raw.length < MAX_STDIN) {
const remaining = MAX_STDIN - raw.length;
raw += chunk.substring(0, remaining);
}
});
process.stdin.on('end', () => {
const result = run(raw);
if (result.stderr) process.stderr.write(`${result.stderr}\n`);
process.stdout.write(typeof result.stdout === 'string' ? result.stdout : raw);
process.exit(Number.isInteger(result.exitCode) ? result.exitCode : 0);
});
}

View File

@@ -0,0 +1,82 @@
/**
* Tests for scripts/hooks/design-quality-check.js
*
* Run with: node tests/hooks/design-quality-check.test.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const hook = require('../../scripts/hooks/design-quality-check');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
let passed = 0;
let failed = 0;
console.log('\nDesign Quality Hook Tests');
console.log('=========================\n');
if (test('passes through non-frontend files silently', () => {
const input = JSON.stringify({ tool_input: { file_path: '/tmp/file.py' } });
const result = hook.run(input);
assert.strictEqual(result.exitCode, 0);
assert.strictEqual(result.stdout, input);
assert.ok(!result.stderr);
})) passed++; else failed++;
if (test('warns for frontend file path', () => {
const tmpFile = path.join(os.tmpdir(), `design-quality-${Date.now()}.tsx`);
fs.writeFileSync(tmpFile, 'export function Hero(){ return <div className="text-center">Get Started</div>; }\n');
try {
const input = JSON.stringify({ tool_input: { file_path: tmpFile } });
const result = hook.run(input);
assert.strictEqual(result.exitCode, 0);
assert.strictEqual(result.stdout, input);
assert.match(result.stderr, /DESIGN CHECK/);
assert.match(result.stderr, /Get Started/);
} finally {
fs.unlinkSync(tmpFile);
}
})) passed++; else failed++;
if (test('handles MultiEdit edits[] payloads', () => {
const tmpFile = path.join(os.tmpdir(), `design-quality-${Date.now()}.css`);
fs.writeFileSync(tmpFile, '.hero{background:linear-gradient(to right,#000,#333)}\n');
try {
const input = JSON.stringify({
tool_input: {
edits: [{ file_path: tmpFile }, { file_path: '/tmp/notes.md' }]
}
});
const result = hook.run(input);
assert.strictEqual(result.exitCode, 0);
assert.strictEqual(result.stdout, input);
assert.match(result.stderr, /frontend file\(s\) modified/);
assert.match(result.stderr, /\.css/);
} finally {
fs.unlinkSync(tmpFile);
}
})) passed++; else failed++;
if (test('returns original stdout on invalid JSON', () => {
const input = '{not valid json';
const result = hook.run(input);
assert.strictEqual(result.exitCode, 0);
assert.strictEqual(result.stdout, input);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);