#
tokens: 47742/50000 52/73 files (page 1/6)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 6. Use http://codebase.md/1yhy/figma-context-mcp?page={x} to view the full context.

# Directory Structure

```
├── .editorconfig
├── .env.example
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .husky
│   ├── commit-msg
│   └── pre-commit
├── .lintstagedrc.json
├── .nvmrc
├── .prettierrc
├── CHANGELOG.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── Dockerfile
├── docs
│   ├── en
│   │   ├── absolute-to-relative-research.md
│   │   ├── architecture.md
│   │   ├── cache-architecture.md
│   │   ├── grid-layout-research.md
│   │   ├── icon-detection.md
│   │   ├── layout-detection-research.md
│   │   └── layout-detection.md
│   └── zh-CN
│       ├── absolute-to-relative-research.md
│       ├── architecture.md
│       ├── cache-architecture.md
│       ├── grid-layout-research.md
│       ├── icon-detection.md
│       ├── layout-detection-research.md
│       ├── layout-detection.md
│       └── TODO-feature-enhancements.md
├── eslint.config.js
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── README.zh-CN.md
├── scripts
│   ├── fetch-test-data.ts
│   └── optimize-figma-json.ts
├── smithery.yaml
├── src
│   ├── algorithms
│   │   ├── icon
│   │   │   ├── detector.ts
│   │   │   └── index.ts
│   │   └── layout
│   │       ├── detector.ts
│   │       ├── index.ts
│   │       ├── optimizer.ts
│   │       └── spatial.ts
│   ├── config.ts
│   ├── core
│   │   ├── effects.ts
│   │   ├── layout.ts
│   │   ├── parser.ts
│   │   └── style.ts
│   ├── index.ts
│   ├── prompts
│   │   ├── design-to-code.ts
│   │   └── index.ts
│   ├── resources
│   │   ├── figma-resources.ts
│   │   └── index.ts
│   ├── server.ts
│   ├── services
│   │   ├── cache
│   │   │   ├── cache-manager.ts
│   │   │   ├── disk-cache.ts
│   │   │   ├── index.ts
│   │   │   ├── lru-cache.ts
│   │   │   └── types.ts
│   │   ├── cache.ts
│   │   ├── figma.ts
│   │   └── simplify-node-response.ts
│   ├── types
│   │   ├── figma.ts
│   │   ├── index.ts
│   │   └── simplified.ts
│   └── utils
│       ├── color.ts
│       ├── css.ts
│       ├── file.ts
│       └── validation.ts
├── tests
│   ├── fixtures
│   │   ├── expected
│   │   │   ├── node-240-32163-optimized.json
│   │   │   ├── node-402-34955-optimized.json
│   │   │   └── real-node-data-optimized.json
│   │   └── figma-data
│   │       ├── node-240-32163.json
│   │       ├── node-402-34955.json
│   │       └── real-node-data.json
│   ├── integration
│   │   ├── __snapshots__
│   │   │   ├── layout-optimization.test.ts.snap
│   │   │   └── output-quality.test.ts.snap
│   │   ├── layout-optimization.test.ts
│   │   ├── output-quality.test.ts
│   │   └── parser.test.ts
│   ├── unit
│   │   ├── algorithms
│   │   │   ├── icon-optimization.test.ts
│   │   │   ├── icon.test.ts
│   │   │   └── layout.test.ts
│   │   ├── resources
│   │   │   └── figma-resources.test.ts
│   │   └── services
│   │       └── cache.test.ts
│   └── utils
│       ├── preview-generator.ts
│       ├── preview.ts
│       ├── run-simplification.ts
│       └── viewer.html
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------

```
v20
```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
{
  "semi": true,
  "trailingComma": "all",
  "singleQuote": false,
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false
}

```

--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------

```json
{
  "src/**/*.{ts,tsx}": ["eslint --fix", "prettier --write"],
  "tests/**/*.{ts,tsx}": ["eslint --fix", "prettier --write"],
  "*.{json,md,yml,yaml}": ["prettier --write"]
}

```

--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------

```
# EditorConfig - https://editorconfig.org
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false

[*.{json,yml,yaml}]
indent_size = 2

[Makefile]
indent_style = tab

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Dependencies
node_modules
.pnpm-store

# Build output
dist

# Environment variables
.env
.env.local
.env.*.local

# IDE
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Testing
coverage
tests/utils/simplified-with-css.json

# OS
.DS_Store
Thumbs.db 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
<h1 align="center">
  <br>
  <img src="https://upload.wikimedia.org/wikipedia/commons/3/33/Figma-logo.svg" alt="Figma MCP" width="80">
  <br>
  Figma Context MCP
  <br>
</h1>

<p align="center">
  <strong>MCP server for seamless Figma design integration with AI coding tools</strong>
</p>

<p align="center">
  <a href="https://smithery.ai/server/@1yhy/Figma-Context-MCP">
    <img src="https://smithery.ai/badge/@1yhy/Figma-Context-MCP" alt="Smithery Badge">
  </a>
  <a href="https://www.npmjs.com/package/@yhy2001/figma-mcp-server">
    <img src="https://img.shields.io/npm/v/@yhy2001/figma-mcp-server" alt="npm version">
  </a>
  <a href="https://github.com/1yhy/Figma-Context-MCP/blob/main/LICENSE">
    <img src="https://img.shields.io/github/license/1yhy/Figma-Context-MCP" alt="License">
  </a>
  <a href="https://github.com/1yhy/Figma-Context-MCP/stargazers">
    <img src="https://img.shields.io/github/stars/1yhy/Figma-Context-MCP" alt="Stars">
  </a>
  <img src="https://img.shields.io/badge/TypeScript-5.7-blue?logo=typescript" alt="TypeScript">
  <img src="https://img.shields.io/badge/MCP-1.24-green" alt="MCP SDK">
</p>

<p align="center">
  <a href="#features">Features</a> •
  <a href="#quick-start">Quick Start</a> •
  <a href="#mcp-capabilities">MCP Capabilities</a> •
  <a href="#architecture">Architecture</a> •
  <a href="#documentation">Documentation</a> •
  <a href="./README.zh-CN.md">中文文档</a>
</p>

---

## What is This?

Figma Context MCP is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that bridges Figma designs with AI coding assistants like [Cursor](https://cursor.sh/), [Windsurf](https://codeium.com/windsurf), and [Cline](https://cline.bot/).

When AI tools can access Figma design data directly, they generate more accurate code on the first try—far better than using screenshots.

> **Note**: This project is based on [Figma-Context-MCP](https://github.com/GLips/Figma-Context-MCP), with optimized data structures and intelligent layout detection algorithms.

## Features

### Core Capabilities

| Capability                      | Description                                                         |
| ------------------------------- | ------------------------------------------------------------------- |
| **Smart Layout Detection**      | Automatically infers Flexbox/Grid layouts from absolute positioning |
| **Icon Merging**                | Intelligently merges vector layers into single exportable icons     |
| **CSS Generation**              | Converts Figma styles to clean, usable CSS                          |
| **Image Export**                | Downloads images and icons with proper naming                       |
| **Multi-layer Caching**         | L1 memory + L2 disk cache to reduce API calls                       |
| **Design-to-Code Prompts**      | Built-in professional prompt templates to guide AI code generation  |
| **Lightweight Resource Access** | Resources API provides low-token data access                        |

### Key Improvements

| Feature          | Before          | After                           |
| ---------------- | --------------- | ------------------------------- |
| Icon exports     | ~45 fragmented  | 2 merged (96% reduction)        |
| Layout detection | Manual absolute | Auto Flexbox/Grid inference     |
| CSS output       | Raw values      | Optimized with defaults removed |
| API calls        | Every request   | 24-hour smart caching           |

## Quick Start

### Prerequisites

- Node.js >= 18.0.0
- A Figma account with API access

### Installation

**Via Smithery (Recommended)**

```bash
npx -y @smithery/cli install @1yhy/Figma-Context-MCP --client claude
```

**Via npm**

```bash
npm install -g @yhy2001/figma-mcp-server
```

**From Source**

```bash
git clone https://github.com/1yhy/Figma-Context-MCP.git
cd Figma-Context-MCP
pnpm install
pnpm build
```

### Configuration

#### 1. Get Figma API Token

1. Go to [Figma Account Settings](https://www.figma.com/settings)
2. Scroll to "Personal access tokens"
3. Click "Create new token"
4. Copy the token

#### 2. Configure Your AI Tool

<details>
<summary><strong>Cursor / Windsurf / Cline</strong></summary>

Add to your MCP configuration file:

```json
{
  "mcpServers": {
    "Figma": {
      "command": "npx",
      "args": ["-y", "@yhy2001/figma-mcp-server", "--stdio"],
      "env": {
        "FIGMA_API_KEY": "your-figma-api-key"
      }
    }
  }
}
```

</details>

<details>
<summary><strong>HTTP/SSE Mode (Local Development)</strong></summary>

```bash
# From source (development)
cp .env.example .env  # Add FIGMA_API_KEY to .env
pnpm install && pnpm build
pnpm start            # Starts on port 3333

# Or with environment variable
FIGMA_API_KEY=<your-key> pnpm start

# Or via global install
figma-mcp --figma-api-key=<your-key> --port=3333

# Connect via SSE
# URL: http://localhost:3333/sse
```

</details>

### Usage Example

```
Please implement this Figma design: https://www.figma.com/design/abc123/MyDesign?node-id=1:234

Use React and Tailwind CSS.
```

---

## MCP Capabilities

This server provides full MCP capabilities support:

```
┌─────────────────────────────────────────────────────────────┐
│                  Figma MCP Server v1.1.0                    │
├─────────────────────────────────────────────────────────────┤
│  Tools (2)                        AI-invoked operations     │
│  ├── get_figma_data              Fetch design data          │
│  └── download_figma_images       Download image assets      │
├─────────────────────────────────────────────────────────────┤
│  Prompts (3)                      User-selected templates   │
│  ├── design_to_code              Full design-to-code flow   │
│  ├── analyze_components          Component structure        │
│  └── extract_styles              Style token extraction     │
├─────────────────────────────────────────────────────────────┤
│  Resources (5)                    Lightweight data sources  │
│  ├── figma://help                Usage guide                │
│  ├── figma://file/{key}          File metadata (~200 tok)   │
│  ├── figma://file/{key}/styles   Design tokens (~500 tok)   │
│  ├── figma://file/{key}/components Component list (~300 tok)│
│  └── figma://file/{key}/assets   Asset inventory (~400 tok) │
└─────────────────────────────────────────────────────────────┘
```

### Tools

| Tool                    | Description                  | Parameters                        |
| ----------------------- | ---------------------------- | --------------------------------- |
| `get_figma_data`        | Fetch simplified design data | `fileKey`, `nodeId?`, `depth?`    |
| `download_figma_images` | Download images and icons    | `fileKey`, `nodes[]`, `localPath` |

### Prompts

Built-in professional prompt templates to help AI generate high-quality code:

| Prompt               | Description                                 | Parameters                         |
| -------------------- | ------------------------------------------- | ---------------------------------- |
| `design_to_code`     | Complete design-to-code workflow            | `framework?`, `includeResponsive?` |
| `analyze_components` | Analyze component structure and reusability | -                                  |
| `extract_styles`     | Extract design tokens                       | -                                  |

**design_to_code workflow includes:**

1. **Project Analysis** - Read theme config, global styles, component library
2. **Structure Analysis** - Identify page patterns, component splitting strategy
3. **ASCII Layout Blueprint** - Generate layout diagram with component and asset annotations
4. **Asset Management** - Analyze, download, and organize images/icons
5. **Code Generation** - Generate code following project conventions
6. **Accessibility Optimization** - Semantic HTML, ARIA labels
7. **Responsive Adaptation** - Mobile layout adjustments

### Resources

Lightweight data access to save tokens:

```bash
# Get file metadata (~200 tokens)
figma://file/abc123

# Get design tokens (~500 tokens)
figma://file/abc123/styles

# Get component list (~300 tokens)
figma://file/abc123/components

# Get asset inventory (~400 tokens)
figma://file/abc123/assets
```

**Resources vs Tools comparison:**

| Feature    | Tools              | Resources             |
| ---------- | ------------------ | --------------------- |
| Controller | AI auto-invoked    | User/client initiated |
| Token cost | Higher (full data) | Lower (summaries)     |
| Use case   | Execute actions    | Browse and explore    |

---

## Architecture

```
┌──────────────────────────────────────────────────────────────┐
│                        MCP Server                            │
├──────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐   │
│  │   Tools     │  │   Prompts   │  │     Resources       │   │
│  │ (2 tools)   │  │ (3 prompts) │  │   (5 resources)     │   │
│  └──────┬──────┘  └─────────────┘  └──────────┬──────────┘   │
│         │                                      │              │
│         └──────────────────┬───────────────────┘              │
│                            ▼                                  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │                   FigmaService                         │  │
│  │          API Calls • Validation • Error Handling       │  │
│  └────────────────────────┬───────────────────────────────┘  │
│                           │                                  │
│         ┌─────────────────┴─────────────────┐                │
│         ▼                                   ▼                │
│  ┌─────────────────┐              ┌─────────────────────┐    │
│  │   CacheManager  │              │    Parser + Algo    │    │
│  │  L1: LRU Memory │              │  • Layout Detection │    │
│  │  L2: Disk Store │              │  • Icon Merging     │    │
│  └─────────────────┘              │  • CSS Generation   │    │
│                                   └─────────────────────┘    │
└──────────────────────────────────────────────────────────────┘
```

### Cache System

Two-layer cache architecture significantly reduces API calls:

| Layer | Storage    | Capacity              | TTL      | Purpose              |
| ----- | ---------- | --------------------- | -------- | -------------------- |
| L1    | Memory LRU | 100 nodes / 50 images | 5-10 min | Hot data fast access |
| L2    | Disk       | 500MB                 | 24 hours | Persistent cache     |

### Layout Detection Algorithm

Automatically converts absolute positioning to semantic Flexbox/Grid layouts:

```
Input (Figma absolute positioning):
┌─────────────────────────┐
│ ■ (10,10)  ■ (110,10)   │
│ ■ (10,60)  ■ (110,60)   │
└─────────────────────────┘

Output (Inferred Grid):
display: grid
grid-template-columns: 100px 100px
grid-template-rows: 50px 50px
gap: 10px
```

---

## Project Structure

```
src/
├── algorithms/           # Smart algorithms
│   ├── layout/          # Layout detection (Flex/Grid inference)
│   └── icon/            # Icon merge detection
├── core/                 # Core parsing
│   ├── parser.ts        # Figma data parser
│   ├── style.ts         # CSS style generation
│   ├── layout.ts        # Layout processing
│   └── effects.ts       # Effects handling
├── services/             # Service layer
│   ├── figma.ts         # Figma API client
│   └── cache/           # Multi-layer cache system
├── prompts/              # MCP prompt templates
├── resources/            # MCP resource handlers
├── types/                # TypeScript type definitions
├── utils/                # Utility functions
├── server.ts             # MCP server main entry
└── index.ts              # CLI entry

tests/
├── fixtures/             # Test data
│   ├── figma-data/      # Raw JSON from Figma API
│   └── expected/        # Expected output snapshots
├── integration/          # Integration tests
│   ├── layout-optimization.test.ts  # Layout optimization tests
│   ├── output-quality.test.ts       # Output quality validation
│   └── parser.test.ts               # Parser tests
└── unit/                 # Unit tests
    ├── algorithms/      # Algorithm tests (layout, icon detection)
    ├── resources/       # Resource handler tests
    └── services/        # Service layer tests

scripts/
└── fetch-test-data.ts   # Figma test data fetcher
```

---

## Documentation

### Core Algorithms

| English                                               | 中文                                               |
| ----------------------------------------------------- | -------------------------------------------------- |
| [Layout Detection](./docs/en/layout-detection.md)     | [布局检测算法](./docs/zh-CN/layout-detection.md)   |
| [Icon Detection](./docs/en/icon-detection.md)         | [图标检测算法](./docs/zh-CN/icon-detection.md)     |
| [Cache Architecture](./docs/en/cache-architecture.md) | [缓存架构设计](./docs/zh-CN/cache-architecture.md) |

### Research Documents

| English                                                             | 中文                                                      |
| ------------------------------------------------------------------- | --------------------------------------------------------- |
| [Grid Layout Research](./docs/en/grid-layout-research.md)           | [Grid 布局研究](./docs/zh-CN/grid-layout-research.md)     |
| [Layout Detection Research](./docs/en/layout-detection-research.md) | [布局检测研究](./docs/zh-CN/layout-detection-research.md) |

### Architecture Documents

| English                                   | 中文                                     |
| ----------------------------------------- | ---------------------------------------- |
| [Architecture](./docs/en/architecture.md) | [系统架构](./docs/zh-CN/architecture.md) |

---

## Command Line Options

| Option            | Description               | Default  |
| ----------------- | ------------------------- | -------- |
| `--figma-api-key` | Figma API token           | Required |
| `--port`          | Server port for HTTP mode | 3333     |
| `--stdio`         | Run in stdio mode         | false    |
| `--help`          | Show help                 | -        |

---

## Contributing

Contributions are welcome!

```bash
# Setup
git clone https://github.com/1yhy/Figma-Context-MCP.git
cd Figma-Context-MCP
pnpm install

# Development
pnpm dev          # Watch mode
pnpm test         # Run tests (272 test cases)
pnpm lint         # Lint code
pnpm build        # Build

# Debug
pnpm inspect      # MCP Inspector

# Test with your own Figma data
pnpm tsx scripts/fetch-test-data.ts <fileKey> <nodeId> <outputName>

# Commit (uses conventional commits)
git commit -m "feat: add new feature"
```

### Commit Types

| Type       | Description   |
| ---------- | ------------- |
| `feat`     | New feature   |
| `fix`      | Bug fix       |
| `docs`     | Documentation |
| `style`    | Code style    |
| `refactor` | Refactoring   |
| `test`     | Tests         |
| `chore`    | Maintenance   |

### Release Process (Maintainers)

```bash
# 1. Update version in package.json and CHANGELOG.md

# 2. Commit version bump
git add -A
git commit -m "chore: bump version to x.x.x"

# 3. Publish to npm (auto runs: type-check → lint → test → build)
npm login --scope=@yhy2001  # if not logged in
pnpm run pub:release

# 4. Create git tag and push
git tag vx.x.x
git push origin main --tags

# 5. Create GitHub Release (optional)
# Go to https://github.com/1yhy/Figma-Context-MCP/releases/new
```

### Testing with Your Own Figma Data

You can test the layout detection and optimization with your own Figma designs:

#### 1. Configure Environment Variables

```bash
# Copy the environment template
cp .env.example .env

# Edit .env file with your configuration
FIGMA_API_KEY=your_figma_api_key_here
TEST_FIGMA_FILE_KEY=your_file_key      # Optional
TEST_FIGMA_NODE_ID=your_node_id        # Optional
```

#### 2. Fetch Figma Node Data

```bash
# Method 1: Using command line arguments (recommended)
pnpm tsx scripts/fetch-test-data.ts <fileKey> <nodeId> <outputName>

# Example: Fetch a specific node
pnpm tsx scripts/fetch-test-data.ts UgtwrncR3GokKDIS7dpm4Z 402-34955 my-design

# Method 2: Using environment variables
TEST_FIGMA_FILE_KEY=xxx TEST_FIGMA_NODE_ID=123-456 pnpm tsx scripts/fetch-test-data.ts
```

**Parameters:**

| Parameter    | Description           | How to Get                                                   |
| ------------ | --------------------- | ------------------------------------------------------------ |
| `fileKey`    | Figma file identifier | Part after `/design/` in URL, e.g., `UgtwrncR3GokKDIS7dpm4Z` |
| `nodeId`     | Node ID               | `node-id=` parameter in URL, e.g., `402-34955`               |
| `outputName` | Output filename       | Custom name, e.g., `my-design`                               |

**Example URL Parsing:**

```
https://www.figma.com/design/UgtwrncR3GokKDIS7dpm4Z/MyProject?node-id=402-34955
                            ↑ fileKey                              ↑ nodeId
```

#### 3. Run Tests to Validate Output

```bash
# Run all tests
pnpm test

# Run only integration tests (validate layout optimization)
pnpm test tests/integration/

# View output JSON files
ls tests/fixtures/figma-data/
```

#### 4. Analyze Optimization Results

Tests automatically validate:

- **Data Compression** - Typically >50% compression
- **Layout Detection** - Flex/Grid layout recognition rate
- **CSS Properties** - Redundant property cleanup
- **Output Quality** - Structural consistency checks

If tests fail, the output may not meet expectations. Check error messages to adjust or report an issue.

---

## License

[MIT](./LICENSE) © 1yhy

## Acknowledgments

- [Figma-Context-MCP](https://github.com/GLips/Figma-Context-MCP) - Original project
- [Model Context Protocol](https://modelcontextprotocol.io/) - MCP specification
- [Best-README-Template](https://github.com/othneildrew/Best-README-Template) - README template reference

---

<p align="center">
  Made with ❤️ for the AI coding community
</p>

```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
# Contributing to Figma Context MCP

Thank you for your interest in contributing to Figma Context MCP! This document provides guidelines and instructions for contributing.

## Table of Contents

- [Code of Conduct](#code-of-conduct)
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Making Changes](#making-changes)
- [Commit Guidelines](#commit-guidelines)
- [Pull Request Process](#pull-request-process)
- [Code Style](#code-style)

## Code of Conduct

Please be respectful and constructive in all interactions. We welcome contributors from all backgrounds and experience levels.

## Getting Started

1. Fork the repository
2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/Figma-Context-MCP.git`
3. Add upstream remote: `git remote add upstream https://github.com/1yhy/Figma-Context-MCP.git`

## Development Setup

### Prerequisites

- Node.js >= 18.0.0
- pnpm (recommended) or npm

### Installation

```bash
# Install dependencies
pnpm install

# Build the project
pnpm build

# Run in development mode (with watch)
pnpm dev
```

### Running Tests

```bash
# Run all tests
pnpm test

# Run specific test suites
pnpm test:layout    # Layout detection tests
pnpm test:icon      # Icon detection tests
pnpm test:all       # All tests including final output
```

### Code Quality

```bash
# Type checking
pnpm type-check

# Linting
pnpm lint
pnpm lint:fix

# Formatting
pnpm format
```

## Making Changes

1. Create a new branch from `main`:

   ```bash
   git checkout -b feat/your-feature-name
   # or
   git checkout -b fix/your-bug-fix
   ```

2. Make your changes following the code style guidelines

3. Test your changes locally

4. Commit your changes following the commit guidelines

## Commit Guidelines

We use [Conventional Commits](https://www.conventionalcommits.org/) for commit messages.

### Format

```
<type>(<scope>): <description>

[optional body]

[optional footer]
```

### Types

| Type       | Description                                        |
| ---------- | -------------------------------------------------- |
| `feat`     | New feature                                        |
| `fix`      | Bug fix                                            |
| `docs`     | Documentation only                                 |
| `style`    | Code style (formatting, missing semi-colons, etc.) |
| `refactor` | Code refactoring (no functional changes)           |
| `perf`     | Performance improvement                            |
| `test`     | Adding or updating tests                           |
| `chore`    | Maintenance tasks                                  |

### Examples

```bash
feat(layout): add support for grid layout detection
fix(parser): handle empty node arrays correctly
docs: update installation instructions
refactor(core): simplify node processing logic
```

## Pull Request Process

1. Update documentation if needed
2. Ensure all tests pass: `pnpm test`
3. Ensure code quality checks pass: `pnpm lint && pnpm type-check`
4. Fill out the pull request template completely
5. Request review from maintainers

### PR Title

Use the same format as commit messages:

```
feat(layout): add support for grid layout detection
```

## Code Style

### TypeScript

- Use TypeScript strict mode
- Prefer `interface` over `type` for object shapes
- Use explicit return types for public functions
- Avoid `any` - use `unknown` if type is truly unknown

### Formatting

- We use Prettier for code formatting
- Run `pnpm format` before committing
- EditorConfig is provided for consistent editor settings

### File Organization

```
src/
├── algorithms/     # Detection algorithms (layout, icon)
├── core/           # Core parsing logic
├── services/       # External services (Figma API, cache)
├── types/          # TypeScript type definitions
└── utils/          # Utility functions
```

### Naming Conventions

| Type       | Convention                            | Example              |
| ---------- | ------------------------------------- | -------------------- |
| Files      | kebab-case                            | `layout-detector.ts` |
| Classes    | PascalCase                            | `LayoutOptimizer`    |
| Functions  | camelCase                             | `detectLayout()`     |
| Constants  | UPPER_SNAKE_CASE                      | `MAX_ICON_SIZE`      |
| Interfaces | PascalCase with `I` prefix (optional) | `SimplifiedNode`     |

## Questions?

If you have questions, feel free to:

- Open an issue with the "question" label
- Check existing issues and discussions

Thank you for contributing!

```

--------------------------------------------------------------------------------
/src/prompts/index.ts:
--------------------------------------------------------------------------------

```typescript
export {
  DESIGN_TO_CODE_PROMPT,
  COMPONENT_ANALYSIS_PROMPT,
  STYLE_EXTRACTION_PROMPT,
} from "./design-to-code.js";

```

--------------------------------------------------------------------------------
/src/services/cache.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Cache Service
 *
 * Re-exports from the cache module for convenience.
 *
 * @module services/cache
 */

export {
  CacheManager,
  cacheManager,
  type CacheConfig,
  type CacheStatistics,
} from "./cache/index.js";

```

--------------------------------------------------------------------------------
/src/resources/index.ts:
--------------------------------------------------------------------------------

```typescript
export {
  getFileMetadata,
  getStyleTokens,
  getComponentList,
  getAssetList,
  createFileMetadataTemplate,
  createStylesTemplate,
  createComponentsTemplate,
  createAssetsTemplate,
  FIGMA_MCP_HELP,
  type FileMetadata,
  type StyleTokens,
  type ComponentSummary,
} from "./figma-resources.js";

```

--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------

```typescript
import { defineConfig } from "tsup";

const isDev = process.env.npm_lifecycle_event === "dev";

export default defineConfig({
  clean: true,
  entry: ["src/index.ts"],
  format: ["esm"],
  minify: !isDev,
  target: "esnext",
  outDir: "dist",
  outExtension: ({ format }) => ({
    js: ".js",
  }),
  onSuccess: isDev ? "node dist/index.js" : undefined,
});

```

--------------------------------------------------------------------------------
/src/algorithms/icon/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Icon Detection Algorithm Module
 *
 * Exports the icon detection algorithm for identifying and merging
 * fragmented icon layers in Figma designs.
 *
 * @module algorithms/icon
 */

export {
  detectIcon,
  processNodeTree,
  collectExportableIcons,
  analyzeNodeTree,
  DEFAULT_CONFIG,
  type FigmaNode,
  type IconDetectionResult,
  type DetectionConfig,
} from "./detector.js";

```

--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------

```typescript
import { defineConfig } from "vitest/config";
import { resolve } from "path";

export default defineConfig({
  test: {
    globals: true,
    environment: "node",
    include: ["tests/**/*.test.ts"],
    coverage: {
      provider: "v8",
      reporter: ["text", "json", "html"],
      include: ["src/**/*.ts"],
      exclude: ["src/index.ts", "src/types/**"],
    },
    testTimeout: 10000,
  },
  resolve: {
    alias: {
      "~": resolve(__dirname, "./src"),
    },
  },
});

```

--------------------------------------------------------------------------------
/src/services/simplify-node-response.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Simplified Node Response Service
 *
 * This module re-exports the core parser functionality for backward compatibility.
 * The actual parsing logic has been moved to ~/core/parser.ts.
 *
 * @module services/simplify-node-response
 */

// Re-export parser function
export { parseFigmaResponse } from "~/core/parser.js";

// Re-export types for backward compatibility
export type {
  CSSStyle,
  TextStyle,
  SimplifiedDesign,
  SimplifiedNode,
  SimplifiedFill,
  ExportInfo,
  ImageResource,
  FigmaNodeType,
} from "~/types/index.js";

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine

# Install pnpm globally
RUN npm install -g pnpm

# Set working directory
WORKDIR /app

# Copy package files and install dependencies (cache layer)
COPY package.json pnpm-lock.yaml ./
RUN pnpm install

# Copy all source files
COPY . .

# Build the project
RUN pnpm run build

# Install this package globally so that the 'figma-mcp' command is available
RUN npm install -g .

# Expose the port (default 3333)
EXPOSE 3333

# Default command to run the MCP server
CMD [ "figma-mcp", "--stdio" ]

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "~/*": ["./src/*"]
    },

    "target": "ES2020",
    "lib": ["ES2021", "DOM"],
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "resolveJsonModule": true,
    "allowJs": true,
    "checkJs": true,

    /* EMIT RULES */
    "outDir": "./dist",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "removeComments": true,

    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*", "tests/**/*"]
}

```

--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Figma Context MCP - Type Definitions
 *
 * Central export point for all type definitions.
 * Types are organized into separate modules by domain.
 *
 * @module types
 */

// Figma API types
export type {
  FigmaNodeType,
  ImageResource,
  ExportFormat,
  ExportInfo,
  FigmaError,
  RateLimitInfo,
  FetchImageParams,
  FetchImageFillParams,
} from "./figma.js";

// Simplified output types
export type {
  CSSHexColor,
  CSSRGBAColor,
  CSSStyle,
  TextStyle,
  SimplifiedFill,
  SimplifiedNode,
  SimplifiedDesign,
  IconDetectionResult,
  IconDetectionConfig,
  LayoutInfo,
} from "./simplified.js";

```

--------------------------------------------------------------------------------
/src/services/cache/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Cache Module
 *
 * Multi-layer caching system for Figma data.
 *
 * @module services/cache
 */

// Types
export type {
  CacheConfig,
  MemoryCacheConfig,
  DiskCacheConfig,
  CacheEntryMeta,
  NodeCacheEntry,
  ImageCacheEntry,
  MemoryCacheStats,
  DiskCacheStats,
  CacheStatistics,
} from "./types.js";

export { DEFAULT_MEMORY_CONFIG, DEFAULT_DISK_CONFIG, DEFAULT_CACHE_CONFIG } from "./types.js";

// LRU Cache
export { LRUCache, NodeLRUCache, type LRUCacheConfig, type CacheStats } from "./lru-cache.js";

// Disk Cache
export { DiskCache } from "./disk-cache.js";

// Cache Manager
export { CacheManager, cacheManager } from "./cache-manager.js";

```

--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------

```javascript
export default {
  extends: ["@commitlint/config-conventional"],
  rules: {
    "type-enum": [
      2,
      "always",
      [
        "feat",     // New feature
        "fix",      // Bug fix
        "docs",     // Documentation
        "style",    // Code style (formatting, etc.)
        "refactor", // Code refactoring
        "perf",     // Performance improvement
        "test",     // Tests
        "build",    // Build system
        "ci",       // CI configuration
        "chore",    // Maintenance
        "revert",   // Revert commit
      ],
    ],
    "subject-case": [0], // Disable case checking for Chinese commits
    "header-max-length": [2, "always", 100],
  },
};

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required:
      - figmaApiKey
    properties:
      figmaApiKey:
        type: string
        description: Your Figma API access token
      port:
        type: number
        default: 3333
        description: Port for the server to run on (default 3333)
  commandFunction:
    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
    |-
    (config) => ({
      command: 'figma-mcp',
      args: [`--figma-api-key=${config.figmaApiKey}`, '--stdio', `--port=${config.port}`],
      env: {}
    })
  exampleConfig:
    figmaApiKey: dummy-figma-api-key
    port: 3333

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { FigmaMcpServer } from "./server.js";
import { getServerConfig } from "./config.js";
import { resolve } from "path";
import { config } from "dotenv";

// Load .env from the current working directory
config({ path: resolve(process.cwd(), ".env") });

export async function startServer(): Promise<void> {
  // Check if we're running in stdio mode (e.g., via CLI)
  const isStdioMode = process.env.NODE_ENV === "cli" || process.argv.includes("--stdio");

  const config = getServerConfig(isStdioMode);

  const server = new FigmaMcpServer(config.figmaApiKey);

  if (isStdioMode) {
    const transport = new StdioServerTransport();
    await server.connect(transport);
  } else {
    console.log(`Initializing Figma MCP Server in HTTP mode on port ${config.port}...`);
    await server.startHttpServer(config.port);
  }
}

startServer().catch((error) => {
  console.error("Failed to start server:", error);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------

```markdown
## Description

<!-- Describe your changes in detail -->

## Related Issue

<!-- Link to the issue this PR addresses (if applicable) -->
<!-- Fixes #123 -->

## Type of Change

<!-- Mark the relevant option with an "x" -->

- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
- [ ] Refactoring (no functional changes)

## Checklist

- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have added/updated comments for hard-to-understand areas
- [ ] I have updated the documentation (if applicable)
- [ ] My changes generate no new warnings
- [ ] I have run `pnpm lint` and `pnpm build` successfully
- [ ] I have tested my changes locally

## Screenshots (if applicable)

<!-- Add screenshots to help explain your changes -->

## Additional Notes

<!-- Any additional information that reviewers should know -->

```

--------------------------------------------------------------------------------
/src/core/style.ts:
--------------------------------------------------------------------------------

```typescript
import { type Node as FigmaDocumentNode } from "@figma/rest-api-spec";
import type { SimplifiedFill } from "~/types/index.js";
import { generateCSSShorthand } from "~/utils/css.js";
import { isVisible } from "~/utils/validation.js";
import { parsePaint } from "~/utils/color.js";
import { hasValue, isStrokeWeights } from "~/utils/validation.js";
export type SimplifiedStroke = {
  colors: SimplifiedFill[];
  strokeWeight?: string;
  strokeDashes?: number[];
  strokeWeights?: string;
};
export function buildSimplifiedStrokes(n: FigmaDocumentNode): SimplifiedStroke {
  const strokes: SimplifiedStroke = { colors: [] };
  if (hasValue("strokes", n) && Array.isArray(n.strokes) && n.strokes.length) {
    strokes.colors = n.strokes.filter(isVisible).map(parsePaint);
  }

  if (hasValue("strokeWeight", n) && typeof n.strokeWeight === "number" && n.strokeWeight > 0) {
    strokes.strokeWeight = `${n.strokeWeight}px`;
  }

  if (hasValue("strokeDashes", n) && Array.isArray(n.strokeDashes) && n.strokeDashes.length) {
    strokes.strokeDashes = n.strokeDashes;
  }

  if (hasValue("individualStrokeWeights", n, isStrokeWeights)) {
    strokes.strokeWeight = generateCSSShorthand(n.individualStrokeWeights);
  }

  return strokes;
}

```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  prettierConfig,
  {
    ignores: ["dist/**", "node_modules/**", "*.config.js"],
  },
  {
    languageOptions: {
      ecmaVersion: 2022,
      sourceType: "module",
      parserOptions: {
        project: "./tsconfig.json",
      },
    },
    rules: {
      // TypeScript specific
      "@typescript-eslint/explicit-function-return-type": "off",
      "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
      "@typescript-eslint/no-explicit-any": "warn",
      "@typescript-eslint/no-non-null-assertion": "warn",
      "@typescript-eslint/consistent-type-imports": [
        "error",
        { prefer: "type-imports", fixStyle: "inline-type-imports" },
      ],

      // General
      "no-console": ["warn", { allow: ["warn", "error"] }],
      "prefer-const": "error",
      "no-var": "error",
      eqeqeq: ["error", "always"],
    },
  },
  // Test files - allow console.log and non-null assertions
  {
    files: ["tests/**/*.ts"],
    rules: {
      "no-console": "off",
      "@typescript-eslint/no-non-null-assertion": "off",
    },
  },
);

```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------

```yaml
name: Feature Request
description: Suggest a new feature or improvement
labels: ["enhancement"]

body:
  - type: markdown
    attributes:
      value: |
        Thanks for suggesting a feature! Please fill out the form below.

  - type: textarea
    id: problem
    attributes:
      label: Problem Statement
      description: Is your feature request related to a problem? Please describe.
      placeholder: "I'm always frustrated when..."
    validations:
      required: true

  - type: textarea
    id: solution
    attributes:
      label: Proposed Solution
      description: Describe the solution you'd like to see.
      placeholder: "I would like to be able to..."
    validations:
      required: true

  - type: textarea
    id: alternatives
    attributes:
      label: Alternatives Considered
      description: Describe any alternative solutions or features you've considered.
      placeholder: "I've also thought about..."

  - type: dropdown
    id: priority
    attributes:
      label: Priority
      description: How important is this feature to you?
      options:
        - Nice to have
        - Important
        - Critical
    validations:
      required: true

  - type: checkboxes
    id: contribution
    attributes:
      label: Contribution
      description: Are you willing to contribute to this feature?
      options:
        - label: I'm willing to submit a PR for this feature

```

--------------------------------------------------------------------------------
/src/algorithms/layout/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Layout Detection Algorithm Module
 *
 * Exports spatial analysis utilities for detecting layout patterns
 * in Figma designs, including row/column grouping and containment analysis.
 *
 * @module algorithms/layout
 */

// Spatial analysis utilities
export {
  RectUtils,
  SpatialProjectionAnalyzer,
  NodeRelationship,
  type Rect,
  type ProjectionLine,
} from "./spatial.js";

// Layout optimizer
export { LayoutOptimizer } from "./optimizer.js";

// Layout detection algorithm
export {
  // Types
  type BoundingBox,
  type ElementRect,
  type LayoutGroup,
  type LayoutAnalysisResult,
  type LayoutNode,
  type GridAnalysisResult,
  type OverlapType,
  type OverlapDetectionResult,
  // Bounding box utilities
  extractBoundingBox,
  toElementRect,
  calculateBounds,
  // Overlap detection
  isOverlappingY,
  isOverlappingX,
  isFullyOverlapping,
  calculateIoU,
  classifyOverlap,
  detectOverlappingElements,
  // Background element detection
  detectBackgroundElement,
  type BackgroundDetectionResult,
  // Grouping
  groupIntoRows,
  groupIntoColumns,
  findOverlappingElements,
  // Gap analysis
  calculateGaps,
  analyzeGaps,
  roundToCommonValue,
  // Alignment
  areValuesAligned,
  analyzeAlignment,
  toJustifyContent,
  toAlignItems,
  // Layout detection
  detectLayoutDirection,
  analyzeLayout,
  buildLayoutTree,
  generateLayoutReport,
  // Grid detection
  clusterValues,
  detectGridLayout,
  // Homogeneity analysis
  analyzeHomogeneity,
  filterHomogeneousForGrid,
  type HomogeneityResult,
} from "./detector.js";

```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
name: CI

on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x, 22.x]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: "pnpm"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Type check
        run: pnpm type-check

      - name: Lint
        run: pnpm lint

      - name: Build
        run: pnpm build

  release:
    needs: build
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20.x
          cache: "pnpm"
          registry-url: "https://registry.npmjs.org"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm build

      # Uncomment to enable auto-publish on main branch
      # - name: Publish to npm
      #   run: npm publish --access public
      #   env:
      #     NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

```

--------------------------------------------------------------------------------
/tests/utils/preview.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Generate simplified output and open viewer for comparison
 *
 * Usage: pnpm preview
 */

import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import { exec } from "child_process";
import { parseFigmaResponse } from "../../src/services/simplify-node-response.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fixturesDir = path.join(__dirname, "../fixtures");

const originalPath = path.join(fixturesDir, "figma-data/real-node-data.json");
const outputPath = path.join(__dirname, "simplified-with-css.json");
const viewerPath = path.join(__dirname, "viewer.html");

async function main() {
  console.log("📖 Reading original Figma data...");
  const originalData = JSON.parse(fs.readFileSync(originalPath, "utf-8"));

  console.log("⚙️  Running simplification...");
  const simplified = parseFigmaResponse(originalData);

  console.log("💾 Saving output...");
  fs.writeFileSync(outputPath, JSON.stringify(simplified, null, 2));

  const originalSize = fs.statSync(originalPath).size;
  const simplifiedSize = fs.statSync(outputPath).size;
  const compressionRate = ((1 - simplifiedSize / originalSize) * 100).toFixed(1);

  console.log("");
  console.log("📊 Results:");
  console.log(`   Original:   ${(originalSize / 1024).toFixed(1)} KB`);
  console.log(`   Simplified: ${(simplifiedSize / 1024).toFixed(1)} KB`);
  console.log(`   Compression: ${compressionRate}%`);
  console.log("");

  // Open viewer in browser
  console.log("🌐 Opening viewer...");
  const openCommand =
    process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";

  exec(`${openCommand} "${viewerPath}"`, (error) => {
    if (error) {
      console.log(`   Viewer path: file://${viewerPath}`);
      console.log("   (Please open manually in browser)");
    } else {
      console.log("   Viewer opened in browser");
    }
  });
}

main().catch(console.error);

```

--------------------------------------------------------------------------------
/src/utils/file.ts:
--------------------------------------------------------------------------------

```typescript
// ==================== Name Processing ====================

/**
 * Clean illegal characters from name (for file names)
 *
 * @param name Original name
 * @param separator Separator, defaults to '_'
 * @returns Sanitized name
 */
export function sanitizeName(name: string, separator: string = "_"): string {
  return name
    .replace(/[/\\?%*:|"<>]/g, separator) // Replace illegal filesystem characters
    .replace(/\s+/g, separator) // Replace whitespace characters
    .replace(new RegExp(`${separator}+`, "g"), separator) // Merge consecutive separators
    .toLowerCase();
}

/**
 * Clean name for ID generation (only keep alphanumeric and hyphens)
 *
 * @param name Original name
 * @returns Sanitized name
 */
export function sanitizeNameForId(name: string): string {
  return name
    .replace(/\s+/g, "-")
    .replace(/[^a-zA-Z0-9-]/g, "")
    .toLowerCase();
}

/**
 * Generate file name based on node name
 */
export function generateFileName(name: string, format: string): string {
  const sanitizedName = sanitizeName(name, "_");
  const lowerFormat = format.toLowerCase();

  // If the name already includes the extension, keep the original name
  if (sanitizedName.includes(`.${lowerFormat}`)) {
    return sanitizedName;
  }

  return `${sanitizedName}.${lowerFormat}`;
}

// ==================== Export Format Detection ====================

/**
 * Node interface for format detection
 */
export interface FormatDetectionNode {
  type?: string;
  exportSettings?: { format?: string[] };
  cssStyles?: { backgroundImage?: string };
  exportInfo?: { format?: string };
  children?: FormatDetectionNode[];
}

/**
 * Choose appropriate export format based on node characteristics
 *
 * @param node Node object
 * @param isSVGNode SVG detection function
 * @returns Recommended export format
 */
export function suggestExportFormat(
  node: FormatDetectionNode,
  isSVGNode: (node: FormatDetectionNode) => boolean,
): "PNG" | "JPG" | "SVG" {
  if (isSVGNode(node)) {
    return "SVG";
  }
  return "PNG";
}

```

--------------------------------------------------------------------------------
/src/types/figma.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Figma API Type Definitions
 *
 * Types related to Figma API interactions, including node types,
 * export formats, and API response structures.
 *
 * @module types/figma
 */

// ==================== Node Types ====================

/** Figma node types */
export type FigmaNodeType =
  | "DOCUMENT"
  | "CANVAS"
  | "FRAME"
  | "GROUP"
  | "TEXT"
  | "VECTOR"
  | "RECTANGLE"
  | "ELLIPSE"
  | "LINE"
  | "POLYGON"
  | "STAR"
  | "BOOLEAN_OPERATION"
  | "REGULAR_POLYGON"
  | "INSTANCE"
  | "COMPONENT"
  | string;

/**
 * Image resource reference
 */
export interface ImageResource {
  /** Image reference ID for downloading */
  imageRef: string;
}

// ==================== Export Types ====================

/**
 * Export format options
 */
export type ExportFormat = "PNG" | "JPG" | "SVG";

/**
 * Export information for image nodes
 */
export interface ExportInfo {
  /** Export type (single image or image group) */
  type: "IMAGE" | "IMAGE_GROUP";
  /** Recommended export format */
  format: ExportFormat;
  /** Node ID for API calls (optional, defaults to node.id) */
  nodeId?: string;
  /** Suggested file name */
  fileName?: string;
}

// ==================== API Types ====================

/**
 * Figma API error
 */
export interface FigmaError {
  status: number;
  err: string;
  rateLimitInfo?: RateLimitInfo;
}

/**
 * Rate limit information from Figma API
 */
export interface RateLimitInfo {
  /** Remaining requests */
  remaining: number | null;
  /** Reset time in seconds */
  resetAfter: number | null;
  /** Retry wait time in seconds */
  retryAfter: number | null;
}

/**
 * Image download parameters
 */
export interface FetchImageParams {
  /** Figma node ID */
  nodeId: string;
  /** Local file name */
  fileName: string;
  /** File format */
  fileType: "png" | "svg";
}

/**
 * Image fill download parameters
 */
export interface FetchImageFillParams {
  /** Node ID */
  nodeId: string;
  /** Local file name */
  fileName: string;
  /** Image reference ID */
  imageRef: string;
}

```

--------------------------------------------------------------------------------
/src/core/effects.ts:
--------------------------------------------------------------------------------

```typescript
import {
  type DropShadowEffect,
  type InnerShadowEffect,
  type BlurEffect,
  type Node as FigmaDocumentNode,
} from "@figma/rest-api-spec";
import { formatRGBAColor } from "~/utils/color.js";
import { hasValue } from "~/utils/validation.js";

export type SimplifiedEffects = {
  boxShadow?: string;
  filter?: string;
  backdropFilter?: string;
};

export function buildSimplifiedEffects(n: FigmaDocumentNode): SimplifiedEffects {
  if (!hasValue("effects", n)) return {};
  const effects = n.effects.filter((e) => e.visible);

  // Handle drop and inner shadows (both go into CSS box-shadow)
  const dropShadows = effects
    .filter((e): e is DropShadowEffect => e.type === "DROP_SHADOW")
    .map(simplifyDropShadow);

  const innerShadows = effects
    .filter((e): e is InnerShadowEffect => e.type === "INNER_SHADOW")
    .map(simplifyInnerShadow);

  const boxShadow = [...dropShadows, ...innerShadows].join(", ");

  // Handle blur effects - separate by CSS property
  // Layer blurs use the CSS 'filter' property
  const filterBlurValues = effects
    .filter((e): e is BlurEffect => e.type === "LAYER_BLUR")
    .map(simplifyBlur)
    .join(" ");

  // Background blurs use the CSS 'backdrop-filter' property
  const backdropFilterValues = effects
    .filter((e): e is BlurEffect => e.type === "BACKGROUND_BLUR")
    .map(simplifyBlur)
    .join(" ");

  const result: SimplifiedEffects = {};
  if (boxShadow) result.boxShadow = boxShadow;
  if (filterBlurValues) result.filter = filterBlurValues;
  if (backdropFilterValues) result.backdropFilter = backdropFilterValues;

  return result;
}

function simplifyDropShadow(effect: DropShadowEffect) {
  return `${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${effect.spread ?? 0}px ${formatRGBAColor(effect.color)}`;
}

function simplifyInnerShadow(effect: InnerShadowEffect) {
  return `inset ${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${effect.spread ?? 0}px ${formatRGBAColor(effect.color)}`;
}

function simplifyBlur(effect: BlurEffect) {
  return `blur(${effect.radius}px)`;
}

```

--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------

```typescript
import { config } from "dotenv";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";

// Load environment variables from .env file
config();

interface ServerConfig {
  figmaApiKey: string;
  port: number;
  configSources: {
    figmaApiKey: "cli" | "env";
    port: "cli" | "env" | "default";
  };
}

function maskApiKey(key: string): string {
  if (key.length <= 4) return "****";
  return `****${key.slice(-4)}`;
}

interface CliArgs {
  "figma-api-key"?: string;
  port?: number;
}

export function getServerConfig(isStdioMode: boolean): ServerConfig {
  // Parse command line arguments
  const argv = yargs(hideBin(process.argv))
    .options({
      "figma-api-key": {
        type: "string",
        description: "Figma API key",
      },
      port: {
        type: "number",
        description: "Port to run the server on",
      },
    })
    .help()
    .version("0.1.12")
    .parseSync() as CliArgs;

  const config: ServerConfig = {
    figmaApiKey: "",
    port: 3333,
    configSources: {
      figmaApiKey: "env",
      port: "default",
    },
  };

  // Handle FIGMA_API_KEY
  if (argv["figma-api-key"]) {
    config.figmaApiKey = argv["figma-api-key"];
    config.configSources.figmaApiKey = "cli";
  } else if (process.env.FIGMA_API_KEY) {
    config.figmaApiKey = process.env.FIGMA_API_KEY;
    config.configSources.figmaApiKey = "env";
  }

  // Handle PORT
  if (argv.port) {
    config.port = argv.port;
    config.configSources.port = "cli";
  } else if (process.env.PORT) {
    config.port = parseInt(process.env.PORT, 10);
    config.configSources.port = "env";
  }

  // Validate configuration
  if (!config.figmaApiKey) {
    console.error("FIGMA_API_KEY is required (via CLI argument --figma-api-key or .env file)");
    process.exit(1);
  }

  // Log configuration sources
  if (!isStdioMode) {
    console.log("\nConfiguration:");
    console.log(
      `- FIGMA_API_KEY: ${maskApiKey(config.figmaApiKey)} (source: ${config.configSources.figmaApiKey})`,
    );
    console.log(`- PORT: ${config.port} (source: ${config.configSources.port})`);
    console.log(); // Empty line for better readability
  }

  return config;
}

```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------

```yaml
name: Bug Report
description: Report a bug or unexpected behavior
labels: ["bug", "triage"]

body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to report a bug! Please fill out the form below.

  - type: textarea
    id: description
    attributes:
      label: Bug Description
      description: A clear and concise description of what the bug is.
      placeholder: Describe the bug...
    validations:
      required: true

  - type: textarea
    id: reproduction
    attributes:
      label: Steps to Reproduce
      description: Steps to reproduce the behavior.
      placeholder: |
        1. Configure MCP server with...
        2. Open Figma file...
        3. Run command...
        4. See error...
    validations:
      required: true

  - type: textarea
    id: expected
    attributes:
      label: Expected Behavior
      description: What did you expect to happen?
      placeholder: Describe what you expected...
    validations:
      required: true

  - type: textarea
    id: actual
    attributes:
      label: Actual Behavior
      description: What actually happened?
      placeholder: Describe what actually happened...
    validations:
      required: true

  - type: input
    id: version
    attributes:
      label: Package Version
      description: What version of figma-mcp-server are you using?
      placeholder: "1.0.0"
    validations:
      required: true

  - type: dropdown
    id: client
    attributes:
      label: AI Client
      description: Which AI client are you using?
      options:
        - Cursor
        - Windsurf
        - Cline
        - Claude Desktop
        - Other
    validations:
      required: true

  - type: input
    id: node-version
    attributes:
      label: Node.js Version
      description: What version of Node.js are you using? (run `node -v`)
      placeholder: "v20.0.0"
    validations:
      required: true

  - type: dropdown
    id: os
    attributes:
      label: Operating System
      options:
        - macOS
        - Windows
        - Linux
    validations:
      required: true

  - type: textarea
    id: logs
    attributes:
      label: Error Logs
      description: If applicable, paste any error logs here.
      render: shell

  - type: textarea
    id: additional
    attributes:
      label: Additional Context
      description: Any other context about the problem.

```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2025-01-06

### Added

- **Grid Layout Detection** - Automatically detect and convert grid-like arrangements to CSS Grid
- **Background Element Merging** - Smart merge of background layers with padding inference
- **Multi-layer Cache System** - LRU memory cache (L1) + disk cache (L2) for 24h data persistence
- **MCP Resources** - Lightweight resource endpoints (`figma://file`, `/styles`, `/components`, `/assets`) for token-efficient metadata browsing
- **MCP Prompts** - Professional `design_to_code` prompt for guided AI code generation workflow
- **Comprehensive Test Suite** - 272 tests covering layout optimization, icon detection, parser, and resources (Vitest)

### Changed

- **Improved Flexbox Detection** - Enhanced stack detection with better gap/padding inference
- **Icon Detection Optimization** - Single-pass tree traversal for better performance
- **Modular Architecture** - Reorganized codebase (`transformers` → `core/algorithms`) for better maintainability
- **Bilingual Documentation** - Complete English and Chinese docs for all algorithms and architecture

### Fixed

- **Gradient Alpha Channel** - Preserve alpha values in gradient color stops
- **Non-grid Element Positioning** - Correct position handling for elements outside grid containers
- **Security Dependencies** - Updated dependencies to resolve vulnerabilities

## [1.0.1] - 2024-12-05

### Added

- Smart layout detection algorithm (Flexbox inference from absolute positioning)
- Icon layer merge algorithm (reduces fragmented exports by 96%)
- CSS generation with optimized output
- HTML preview generation from Figma JSON
- Comprehensive documentation for algorithms

### Changed

- Optimized data structures for AI consumption
- Reduced output size by ~87% through intelligent simplification
- Improved node processing with better type handling

### Fixed

- Round all px values to integers
- Proper handling of gradient and image fills
- Border style extraction improvements

## [1.0.0] - 2024-12-01

### Added

- Initial release
- MCP server implementation for Figma integration
- `get_figma_data` tool for fetching design data
- `download_figma_images` tool for image export
- Support for stdio and HTTP/SSE modes
- Basic CSS style generation
- Figma API integration with caching

[1.1.0]: https://github.com/1yhy/Figma-Context-MCP/compare/v1.0.1...v1.1.0
[1.0.1]: https://github.com/1yhy/Figma-Context-MCP/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/1yhy/Figma-Context-MCP/releases/tag/v1.0.0

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@yhy2001/figma-mcp-server",
  "version": "1.1.0",
  "description": "MCP server for Figma design integration with AI coding tools",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "figma-mcp": "dist/index.js"
  },
  "files": [
    "dist",
    "README.md",
    "README.zh-CN.md"
  ],
  "scripts": {
    "dev": "cross-env NODE_ENV=development tsup --watch",
    "build": "tsup",
    "start": "node dist/index.js",
    "start:cli": "cross-env NODE_ENV=cli node dist/index.js",
    "start:http": "node dist/index.js",
    "dev:cli": "cross-env NODE_ENV=development tsup --watch -- --stdio",
    "type-check": "tsc --noEmit",
    "lint": "eslint src/",
    "lint:fix": "eslint src/ --fix",
    "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
    "format:check": "prettier --check \"src/**/*.ts\"",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:ui": "vitest --ui",
    "test:unit": "vitest run tests/unit",
    "test:integration": "vitest run tests/integration",
    "preview": "tsx tests/utils/preview.ts",
    "inspect": "pnpx @modelcontextprotocol/inspector",
    "mcp-test": "pnpm start -- --stdio",
    "prepublishOnly": "pnpm type-check && pnpm lint && pnpm test && pnpm build",
    "pub:release": "pnpm build && npm publish --access public",
    "publish:local": "pnpm build && npm pack",
    "prepare": "husky || true"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/1yhy/Figma-Context-MCP.git"
  },
  "homepage": "https://github.com/1yhy/Figma-Context-MCP#readme",
  "bugs": {
    "url": "https://github.com/1yhy/Figma-Context-MCP/issues"
  },
  "keywords": [
    "figma",
    "mcp",
    "model-context-protocol",
    "typescript",
    "ai",
    "design",
    "cursor",
    "windsurf",
    "cline",
    "design-to-code"
  ],
  "author": "1yhy",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.24.3",
    "cross-env": "^7.0.3",
    "dotenv": "^16.4.7",
    "express": "^4.21.2",
    "remeda": "^2.20.1",
    "yargs": "^17.7.2",
    "zod": "^4.1.13"
  },
  "devDependencies": {
    "@commitlint/cli": "^20.2.0",
    "@commitlint/config-conventional": "^19.0.0",
    "@eslint/js": "^9.39.1",
    "@figma/rest-api-spec": "^0.24.0",
    "@types/express": "^5.0.0",
    "@types/node": "^24.10.1",
    "@types/yargs": "^17.0.33",
    "@typescript-eslint/eslint-plugin": "^8.48.1",
    "@typescript-eslint/parser": "^8.48.1",
    "@vitest/coverage-v8": "^4.0.15",
    "eslint": "^9.39.1",
    "eslint-config-prettier": "^10.0.1",
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0",
    "prettier": "^3.5.0",
    "tsup": "^8.5.1",
    "tsx": "^4.21.0",
    "typescript": "^5.7.3",
    "typescript-eslint": "^8.48.1",
    "vitest": "^4.0.15"
  }
}

```

--------------------------------------------------------------------------------
/src/utils/color.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Color Conversion Utilities
 *
 * Functions for converting between Figma color formats and CSS color values.
 *
 * @module utils/color
 */

import type { Paint, RGBA } from "@figma/rest-api-spec";
import type { CSSHexColor, CSSRGBAColor, SimplifiedFill } from "~/types/index.js";

// ==================== Type Definitions ====================

export interface ColorValue {
  hex: CSSHexColor;
  opacity: number;
}

// ==================== Color Conversion ====================

/**
 * Convert hex color and opacity to rgba format
 */
export function hexToRgba(hex: string, opacity: number = 1): string {
  hex = hex.replace("#", "");

  if (hex.length === 3) {
    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
  }

  const r = parseInt(hex.substring(0, 2), 16);
  const g = parseInt(hex.substring(2, 4), 16);
  const b = parseInt(hex.substring(4, 6), 16);
  const validOpacity = Math.min(Math.max(opacity, 0), 1);

  return `rgba(${r}, ${g}, ${b}, ${validOpacity})`;
}

/**
 * Convert Figma RGBA color to { hex, opacity }
 */
export function convertColor(color: RGBA, opacity = 1): ColorValue {
  const r = Math.round(color.r * 255);
  const g = Math.round(color.g * 255);
  const b = Math.round(color.b * 255);
  const a = Math.round(opacity * color.a * 100) / 100;

  const hex = ("#" +
    ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()) as CSSHexColor;

  return { hex, opacity: a };
}

/**
 * Convert Figma RGBA to CSS rgba() format
 */
export function formatRGBAColor(color: RGBA, opacity = 1): CSSRGBAColor {
  const r = Math.round(color.r * 255);
  const g = Math.round(color.g * 255);
  const b = Math.round(color.b * 255);
  const a = Math.round(opacity * color.a * 100) / 100;

  return `rgba(${r}, ${g}, ${b}, ${a})`;
}

// ==================== Fill Parsing ====================

/**
 * Convert Figma Paint to simplified fill format
 */
export function parsePaint(raw: Paint): SimplifiedFill {
  if (raw.type === "IMAGE") {
    const imagePaint = raw as { type: "IMAGE"; imageRef?: string; scaleMode?: string };
    return {
      type: "IMAGE",
      imageRef: imagePaint.imageRef,
      scaleMode: imagePaint.scaleMode,
    };
  }

  if (raw.type === "SOLID") {
    const { hex, opacity } = convertColor(raw.color!, raw.opacity);
    if (opacity === 1) {
      return hex;
    }
    return formatRGBAColor(raw.color!, opacity);
  }

  if (
    raw.type === "GRADIENT_LINEAR" ||
    raw.type === "GRADIENT_RADIAL" ||
    raw.type === "GRADIENT_ANGULAR" ||
    raw.type === "GRADIENT_DIAMOND"
  ) {
    const gradientPaint = raw as {
      type: typeof raw.type;
      gradientHandlePositions?: Array<{ x: number; y: number }>;
      gradientStops?: Array<{ position: number; color: RGBA }>;
    };
    return {
      type: raw.type,
      gradientHandlePositions: gradientPaint.gradientHandlePositions,
      gradientStops: gradientPaint.gradientStops?.map(({ position, color }) => ({
        position,
        color: convertColor(color).hex,
      })),
    };
  }

  throw new Error(`Unknown paint type: ${raw.type}`);
}

```

--------------------------------------------------------------------------------
/src/services/cache/types.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Cache System Type Definitions
 *
 * @module services/cache/types
 */

// ==================== Configuration ====================

/**
 * Memory cache configuration
 */
export interface MemoryCacheConfig {
  /** Maximum number of node cache items (default: 100) */
  maxNodeItems: number;
  /** Maximum number of image cache items (default: 50) */
  maxImageItems: number;
  /** Node cache TTL in milliseconds (default: 5 minutes) */
  nodeTTL: number;
  /** Image cache TTL in milliseconds (default: 10 minutes) */
  imageTTL: number;
}

/**
 * Disk cache configuration
 */
export interface DiskCacheConfig {
  /** Cache directory path */
  cacheDir: string;
  /** Maximum disk cache size in bytes (default: 500MB) */
  maxSize: number;
  /** Cache TTL in milliseconds (default: 24 hours) */
  ttl: number;
}

/**
 * Complete cache configuration
 */
export interface CacheConfig {
  /** Whether caching is enabled */
  enabled: boolean;
  /** Memory cache configuration */
  memory: MemoryCacheConfig;
  /** Disk cache configuration */
  disk: DiskCacheConfig;
}

// ==================== Cache Entries ====================

/**
 * Cache entry metadata
 */
export interface CacheEntryMeta {
  /** Cache key */
  key: string;
  /** Creation timestamp */
  createdAt: number;
  /** Expiration timestamp */
  expiresAt: number;
  /** Figma file key */
  fileKey: string;
  /** Figma node ID (optional) */
  nodeId?: string;
  /** Figma file version (lastModified) */
  version?: string;
  /** Query depth */
  depth?: number;
  /** Data size in bytes */
  size?: number;
}

/**
 * Node cache entry
 */
export interface NodeCacheEntry {
  /** Cached data */
  data: unknown;
  /** Figma file key */
  fileKey: string;
  /** Figma node ID */
  nodeId?: string;
  /** Figma file version */
  version?: string;
  /** Query depth */
  depth?: number;
}

/**
 * Image cache entry
 */
export interface ImageCacheEntry {
  /** Local file path */
  path: string;
  /** Figma file key */
  fileKey: string;
  /** Figma node ID */
  nodeId: string;
  /** Image format */
  format: string;
  /** File size in bytes */
  size?: number;
}

// ==================== Statistics ====================

/**
 * Memory cache statistics
 */
export interface MemoryCacheStats {
  /** Cache hits */
  hits: number;
  /** Cache misses */
  misses: number;
  /** Current item count */
  size: number;
  /** Maximum item count */
  maxSize: number;
  /** Hit rate (0-1) */
  hitRate: number;
  /** Eviction count */
  evictions: number;
}

/**
 * Disk cache statistics
 */
export interface DiskCacheStats {
  /** Cache hits */
  hits: number;
  /** Cache misses */
  misses: number;
  /** Total size in bytes */
  totalSize: number;
  /** Maximum size in bytes */
  maxSize: number;
  /** Node data file count */
  nodeFileCount: number;
  /** Image file count */
  imageFileCount: number;
}

/**
 * Combined cache statistics
 */
export interface CacheStatistics {
  /** Whether cache is enabled */
  enabled: boolean;
  /** Memory cache stats */
  memory: MemoryCacheStats;
  /** Disk cache stats */
  disk: DiskCacheStats;
}

// ==================== Default Configurations ====================

/**
 * Default memory cache configuration
 */
export const DEFAULT_MEMORY_CONFIG: MemoryCacheConfig = {
  maxNodeItems: 100,
  maxImageItems: 50,
  nodeTTL: 5 * 60 * 1000, // 5 minutes
  imageTTL: 10 * 60 * 1000, // 10 minutes
};

/**
 * Default disk cache configuration
 */
export const DEFAULT_DISK_CONFIG: Partial<DiskCacheConfig> = {
  maxSize: 500 * 1024 * 1024, // 500MB
  ttl: 24 * 60 * 60 * 1000, // 24 hours
};

/**
 * Default complete cache configuration
 */
export const DEFAULT_CACHE_CONFIG: Omit<CacheConfig, "disk"> & { disk: Partial<DiskCacheConfig> } =
  {
    enabled: true,
    memory: DEFAULT_MEMORY_CONFIG,
    disk: DEFAULT_DISK_CONFIG,
  };

```

--------------------------------------------------------------------------------
/scripts/optimize-figma-json.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env npx tsx
/**
 * Figma JSON Optimizer Script
 *
 * Usage:
 *   npx tsx scripts/optimize-figma-json.ts <input-file> [output-file]
 *
 * Examples:
 *   npx tsx scripts/optimize-figma-json.ts tests/fixtures/figma-data/node-108-517.json
 *   npx tsx scripts/optimize-figma-json.ts input.json output-optimized.json
 *
 * If output-file is not specified:
 *   - For files in figma-data/, outputs to expected/<name>-optimized.json
 *   - Otherwise, outputs to <input-name>-optimized.json in the same directory
 */

import fs from "fs";
import path from "path";
import { parseFigmaResponse } from "../src/core/parser.js";

// Parse arguments
const args = process.argv.slice(2);

if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
  console.log(`
Figma JSON Optimizer

Usage:
  npx tsx scripts/optimize-figma-json.ts <input-file> [output-file]

Examples:
  npx tsx scripts/optimize-figma-json.ts tests/fixtures/figma-data/node-108-517.json
  npx tsx scripts/optimize-figma-json.ts input.json output-optimized.json

Options:
  --help, -h     Show this help message
  --stats        Show detailed statistics only (no file output)
  --quiet, -q    Minimal output
`);
  process.exit(0);
}

const inputFile = args[0];
const showStatsOnly = args.includes("--stats");
const quiet = args.includes("--quiet") || args.includes("-q");

// Determine output file
let outputFile = args.find((arg) => !arg.startsWith("-") && arg !== inputFile);

if (!outputFile && !showStatsOnly) {
  const inputDir = path.dirname(inputFile);
  const inputName = path.basename(inputFile, ".json");

  // If input is in figma-data, output to expected
  if (inputDir.includes("figma-data")) {
    const expectedDir = inputDir.replace("figma-data", "expected");
    outputFile = path.join(expectedDir, `${inputName}-optimized.json`);
  } else {
    outputFile = path.join(inputDir, `${inputName}-optimized.json`);
  }
}

// Check input file exists
if (!fs.existsSync(inputFile)) {
  console.error(`Error: Input file not found: ${inputFile}`);
  process.exit(1);
}

// Read and optimize
if (!quiet) {
  console.log(`\nOptimizing: ${inputFile}`);
}

const startTime = Date.now();
const rawData = JSON.parse(fs.readFileSync(inputFile, "utf8"));
const optimized = parseFigmaResponse(rawData);
const elapsed = Date.now() - startTime;

// Calculate statistics
const json = JSON.stringify(optimized, null, 2);
const originalSize = fs.statSync(inputFile).size;
const optimizedSize = Buffer.byteLength(json);
const compression = ((1 - optimizedSize / originalSize) * 100).toFixed(1);

const absoluteCount = (json.match(/"position":\s*"absolute"/g) || []).length;
const gridCount = (json.match(/"display":\s*"grid"/g) || []).length;
const flexCount = (json.match(/"display":\s*"flex"/g) || []).length;
const flexRowCount = (json.match(/"flexDirection":\s*"row"/g) || []).length;
const flexColumnCount = (json.match(/"flexDirection":\s*"column"/g) || []).length;

// Get root node info
let rootInfo = "";
if (optimized.nodes && optimized.nodes.length > 0) {
  const root = optimized.nodes[0];
  rootInfo = `${root.name} (${root.cssStyles?.width} × ${root.cssStyles?.height})`;
}

// Output results
if (!quiet) {
  console.log(`\n${"─".repeat(50)}`);
  console.log(`Root: ${rootInfo}`);
  console.log(`${"─".repeat(50)}`);
  console.log(`\nLayout Statistics:`);
  console.log(`  position:absolute  ${absoluteCount}`);
  console.log(`  display:grid       ${gridCount}`);
  console.log(`  display:flex       ${flexCount} (row: ${flexRowCount}, column: ${flexColumnCount})`);
  console.log(`\nCompression:`);
  console.log(`  Original:  ${(originalSize / 1024).toFixed(1)} KB`);
  console.log(`  Optimized: ${(optimizedSize / 1024).toFixed(1)} KB`);
  console.log(`  Reduced:   ${compression}%`);
  console.log(`\nTime: ${elapsed}ms`);
}

// Save output
if (!showStatsOnly && outputFile) {
  // Ensure output directory exists
  const outputDir = path.dirname(outputFile);
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  fs.writeFileSync(outputFile, json);

  if (!quiet) {
    console.log(`\nSaved: ${outputFile}`);
  } else {
    console.log(outputFile);
  }
}

if (!quiet) {
  console.log("");
}

```

--------------------------------------------------------------------------------
/src/utils/validation.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Validation Utilities
 *
 * Type guards, validators, and visibility checks for Figma nodes.
 *
 * @module utils/validation
 */

import type {
  Rectangle,
  HasLayoutTrait,
  StrokeWeights,
  HasFramePropertiesTrait,
} from "@figma/rest-api-spec";
import { isTruthy } from "remeda";
import type { CSSHexColor, CSSRGBAColor } from "~/types/index.js";

export { isTruthy };

// ==================== Visibility Types ====================

/** Properties for visibility checking */
export interface VisibilityProperties {
  visible?: boolean;
  opacity?: number;
  absoluteBoundingBox?: { x: number; y: number; width: number; height: number };
  absoluteRenderBounds?: { x: number; y: number; width: number; height: number } | null;
}

/** Properties for parent container clipping check */
export interface ParentClipProperties {
  clipsContent?: boolean;
  absoluteBoundingBox?: { x: number; y: number; width: number; height: number };
}

// ==================== Visibility Checks ====================

/**
 * Check if an element is visible
 */
export function isVisible(element: VisibilityProperties): boolean {
  if (element.visible === false) {
    return false;
  }

  if (element.opacity === 0) {
    return false;
  }

  if (element.absoluteRenderBounds === null) {
    return false;
  }

  return true;
}

/**
 * Check if an element is visible within its parent container (considering clipping)
 */
export function isVisibleInParent(
  element: VisibilityProperties,
  parent: ParentClipProperties,
): boolean {
  if (!isVisible(element)) {
    return false;
  }

  if (
    parent &&
    parent.clipsContent === true &&
    element.absoluteBoundingBox &&
    parent.absoluteBoundingBox
  ) {
    const elementBox = element.absoluteBoundingBox;
    const parentBox = parent.absoluteBoundingBox;

    const outsideParent =
      elementBox.x >= parentBox.x + parentBox.width ||
      elementBox.x + elementBox.width <= parentBox.x ||
      elementBox.y >= parentBox.y + parentBox.height ||
      elementBox.y + elementBox.height <= parentBox.y;

    if (outsideParent) {
      return false;
    }
  }

  return true;
}

// ==================== Object Processing ====================

/**
 * Remove empty arrays and empty objects from an object
 */
export function removeEmptyKeys<T>(input: T): T {
  if (typeof input !== "object" || input === null) {
    return input;
  }

  if (Array.isArray(input)) {
    return input.map((item) => removeEmptyKeys(item)) as T;
  }

  const result = {} as T;
  for (const key in input) {
    if (Object.prototype.hasOwnProperty.call(input, key)) {
      const value = input[key];
      const cleanedValue = removeEmptyKeys(value);

      if (
        cleanedValue !== undefined &&
        !(Array.isArray(cleanedValue) && cleanedValue.length === 0) &&
        !(
          typeof cleanedValue === "object" &&
          cleanedValue !== null &&
          Object.keys(cleanedValue).length === 0
        )
      ) {
        result[key] = cleanedValue;
      }
    }
  }

  return result;
}

// ==================== Type Guards ====================

export function hasValue<K extends PropertyKey, T>(
  key: K,
  obj: unknown,
  typeGuard?: (val: unknown) => val is T,
): obj is Record<K, T> {
  const isObject = typeof obj === "object" && obj !== null;
  if (!isObject || !(key in obj)) return false;
  const val = (obj as Record<K, unknown>)[key];
  return typeGuard ? typeGuard(val) : val !== undefined;
}

export function isFrame(val: unknown): val is HasFramePropertiesTrait {
  return (
    typeof val === "object" &&
    !!val &&
    "clipsContent" in val &&
    typeof val.clipsContent === "boolean"
  );
}

export function isLayout(val: unknown): val is HasLayoutTrait {
  return (
    typeof val === "object" &&
    !!val &&
    "absoluteBoundingBox" in val &&
    typeof val.absoluteBoundingBox === "object" &&
    !!val.absoluteBoundingBox &&
    "x" in val.absoluteBoundingBox &&
    "y" in val.absoluteBoundingBox &&
    "width" in val.absoluteBoundingBox &&
    "height" in val.absoluteBoundingBox
  );
}

export function isStrokeWeights(val: unknown): val is StrokeWeights {
  return (
    typeof val === "object" &&
    val !== null &&
    "top" in val &&
    "right" in val &&
    "bottom" in val &&
    "left" in val
  );
}

export function isRectangle<T, K extends string>(
  key: K,
  obj: T,
): obj is T & { [P in K]: Rectangle } {
  const recordObj = obj as Record<K, unknown>;
  return (
    typeof obj === "object" &&
    !!obj &&
    key in recordObj &&
    typeof recordObj[key] === "object" &&
    !!recordObj[key] &&
    "x" in recordObj[key] &&
    "y" in recordObj[key] &&
    "width" in recordObj[key] &&
    "height" in recordObj[key]
  );
}

export function isRectangleCornerRadii(val: unknown): val is number[] {
  return Array.isArray(val) && val.length === 4 && val.every((v) => typeof v === "number");
}

export function isCSSColorValue(val: unknown): val is CSSRGBAColor | CSSHexColor {
  return typeof val === "string" && (val.startsWith("#") || val.startsWith("rgba"));
}

```

--------------------------------------------------------------------------------
/src/types/simplified.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Simplified Output Type Definitions
 *
 * Types for the MCP simplified output format, including CSS styles,
 * simplified node structures, and algorithm configurations.
 *
 * @module types/simplified
 */

import type { ExportInfo } from "./figma.js";

// ==================== CSS Types ====================

/** CSS hex color format */
export type CSSHexColor = `#${string}`;

/** CSS rgba color format */
export type CSSRGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`;

/**
 * CSS style object containing all supported CSS properties
 */
export type CSSStyle = {
  // Text styles
  fontFamily?: string;
  fontSize?: string;
  fontWeight?: string | number;
  textAlign?: string;
  verticalAlign?: string;
  lineHeight?: string;

  // Colors and backgrounds
  color?: string;
  backgroundColor?: string;
  background?: string;
  backgroundImage?: string;

  // Layout
  width?: string;
  height?: string;
  margin?: string;
  padding?: string;
  position?: string;
  top?: string;
  right?: string;
  bottom?: string;
  left?: string;
  display?: string;
  flexDirection?: string;
  justifyContent?: string;
  alignItems?: string;
  gap?: string;

  // Grid layout
  gridTemplateColumns?: string;
  gridTemplateRows?: string;
  rowGap?: string;
  columnGap?: string;
  justifyItems?: string;
  gridColumn?: string;
  gridRow?: string;

  // Flex item
  flexGrow?: string | number;
  flexShrink?: string | number;
  flexBasis?: string;
  flex?: string;
  alignSelf?: string;
  order?: string | number;

  // Borders and radius
  border?: string;
  borderRadius?: string;
  borderWidth?: string;
  borderStyle?: string;
  borderColor?: string;
  borderImage?: string;
  borderImageSlice?: string;

  // Effects
  boxShadow?: string;
  filter?: string;
  backdropFilter?: string;
  opacity?: string;

  // Webkit specific
  webkitBackgroundClip?: string;
  webkitTextFillColor?: string;
  backgroundClip?: string;

  // Allow additional properties
  [key: string]: string | number | undefined;
};

/**
 * Text style properties (legacy, for backward compatibility)
 */
export type TextStyle = Partial<{
  fontFamily: string;
  fontWeight: number;
  fontSize: number;
  textAlignHorizontal: string;
  textAlignVertical: string;
  lineHeightPx: number;
}>;

// ==================== Simplified Node Types ====================

/**
 * Fill type for simplified nodes
 */
export type SimplifiedFill =
  | CSSHexColor
  | CSSRGBAColor
  | SimplifiedSolidFill
  | SimplifiedGradientFill
  | SimplifiedImageFill;

/** Solid fill with explicit type */
export interface SimplifiedSolidFill {
  type: "SOLID";
  color: string;
  opacity?: number;
}

/** Gradient fill */
export interface SimplifiedGradientFill {
  type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND";
  gradientHandlePositions?: Array<{ x: number; y: number }>;
  gradientStops?: Array<{
    position: number;
    color: string;
  }>;
}

/** Image fill */
export interface SimplifiedImageFill {
  type: "IMAGE";
  imageRef?: string;
  scaleMode?: string;
}

/**
 * Simplified node structure
 * This is the main output type for the MCP response
 */
export interface SimplifiedNode {
  /** Node ID */
  id: string;
  /** Node name */
  name: string;
  /** Node type (FRAME, TEXT, VECTOR, etc.) */
  type: string;
  /** Text content (for TEXT nodes) */
  text?: string;
  /** Legacy text style (for backward compatibility) */
  style?: TextStyle;
  /** CSS styles */
  cssStyles?: CSSStyle;
  /** Fill information */
  fills?: SimplifiedFill[];
  /** Export information (for image nodes) */
  exportInfo?: ExportInfo;
  /** Child nodes */
  children?: SimplifiedNode[];
  /** Internal: absolute X coordinate */
  _absoluteX?: number;
  /** Internal: absolute Y coordinate */
  _absoluteY?: number;
}

/**
 * Simplified design output
 * Top-level structure returned by the MCP
 */
export interface SimplifiedDesign {
  /** Design file name */
  name: string;
  /** Last modified timestamp */
  lastModified: string;
  /** File version */
  version?: string;
  /** Thumbnail URL */
  thumbnailUrl: string;
  /** Root nodes */
  nodes: SimplifiedNode[];
}

// ==================== Algorithm Types ====================

/**
 * Icon detection result
 */
export interface IconDetectionResult {
  nodeId: string;
  nodeName: string;
  shouldMerge: boolean;
  exportFormat: "SVG" | "PNG";
  reason: string;
  size?: { width: number; height: number };
  childCount?: number;
}

/**
 * Icon detection configuration
 */
export interface IconDetectionConfig {
  /** Maximum icon size in pixels */
  maxIconSize: number;
  /** Minimum icon size in pixels */
  minIconSize: number;
  /** Minimum ratio of mergeable types */
  mergeableRatio: number;
  /** Maximum nesting depth */
  maxDepth: number;
  /** Maximum child count */
  maxChildren: number;
  /** Maximum size to respect exportSettings */
  respectExportSettingsMaxSize: number;
}

/**
 * Layout detection result
 */
export interface LayoutInfo {
  type: "flex" | "absolute" | "grid";
  direction?: "row" | "column";
  gap?: number;
  justifyContent?: string;
  alignItems?: string;
  /** Grid-specific: row gap */
  rowGap?: number;
  /** Grid-specific: column gap */
  columnGap?: number;
  /** Grid-specific: template columns (e.g., "100px 200px 100px") */
  gridTemplateColumns?: string;
  /** Grid-specific: template rows (e.g., "auto auto") */
  gridTemplateRows?: string;
  /** Grid confidence score (0-1) */
  confidence?: number;
}

```

--------------------------------------------------------------------------------
/src/utils/css.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * CSS Utilities
 *
 * CSS output optimization and generation utilities.
 * Used to reduce output size and improve readability.
 *
 * @module utils/css
 */

// ==================== CSS Shorthand Generation ====================

/**
 * Generate CSS shorthand properties (such as padding, margin, border-radius)
 *
 * @example
 * generateCSSShorthand({ top: 10, right: 10, bottom: 10, left: 10 }) // "10px"
 * generateCSSShorthand({ top: 10, right: 20, bottom: 10, left: 20 }) // "10px 20px"
 */
export function generateCSSShorthand(
  values: {
    top: number;
    right: number;
    bottom: number;
    left: number;
  },
  options: {
    ignoreZero?: boolean;
    suffix?: string;
  } = {},
): string | undefined {
  const { ignoreZero = true, suffix = "px" } = options;
  const { top, right, bottom, left } = values;

  if (ignoreZero && top === 0 && right === 0 && bottom === 0 && left === 0) {
    return undefined;
  }

  if (top === right && right === bottom && bottom === left) {
    return `${top}${suffix}`;
  }

  if (right === left) {
    if (top === bottom) {
      return `${top}${suffix} ${right}${suffix}`;
    }
    return `${top}${suffix} ${right}${suffix} ${bottom}${suffix}`;
  }

  return `${top}${suffix} ${right}${suffix} ${bottom}${suffix} ${left}${suffix}`;
}

// ==================== Numeric Precision Optimization ====================

/**
 * Round a number to specified precision
 * @param value Original number
 * @param precision Number of decimal places, default 0 (integer)
 */
export function roundValue(value: number, precision: number = 0): number {
  if (precision === 0) {
    return Math.round(value);
  }
  const multiplier = Math.pow(10, precision);
  return Math.round(value * multiplier) / multiplier;
}

/**
 * Format px value, rounded to integer
 * @param value Pixel value
 */
export function formatPxValue(value: number): string {
  return `${Math.round(value)}px`;
}

/**
 * Format numeric value, used for gap and other properties, rounded to integer
 * @param value Numeric value
 */
export function formatNumericValue(value: number): string {
  return `${Math.round(value)}px`;
}

// ==================== Browser Defaults ====================

/**
 * Browser/Tailwind default values
 * These values can be omitted from output
 */
export const BROWSER_DEFAULTS: Record<string, string | number | undefined> = {
  // Text defaults
  textAlign: "left",
  verticalAlign: "top",
  fontWeight: 400,

  // Flex defaults
  flexDirection: "row",
  justifyContent: "flex-start",
  alignItems: "stretch",

  // Position defaults (if all elements are absolute, can be omitted)
  // position: 'static',  // Not omitting for now, as we explicitly use absolute

  // Other
  opacity: "1",
  borderStyle: "none",
};

/**
 * Check if a value is the default value
 */
export function isDefaultValue(key: string, value: string | number | undefined): boolean {
  if (value === undefined) return true;

  const defaultValue = BROWSER_DEFAULTS[key];
  if (defaultValue === undefined) return false;

  // Handle number and string comparison
  if (typeof defaultValue === "number" && typeof value === "number") {
    return defaultValue === value;
  }

  return String(defaultValue) === String(value);
}

/**
 * Omit default style values
 * @param styles CSS style object
 * @returns Optimized style object
 */
export function omitDefaultStyles<T extends Record<string, unknown>>(styles: T): Partial<T> {
  const result: Partial<T> = {};

  for (const [key, value] of Object.entries(styles)) {
    // Skip undefined
    if (value === undefined) continue;

    // Skip default values
    if (isDefaultValue(key, value as string | number)) continue;

    // Keep non-default values
    (result as Record<string, unknown>)[key] = value;
  }

  return result;
}

// ==================== Gap Analysis ====================

/**
 * Analyze gap consistency
 * @param gaps Array of gaps
 * @param tolerancePercent Tolerance percentage, default 20%
 */
export function analyzeGapConsistency(
  gaps: number[],
  tolerancePercent: number = 20,
): {
  isConsistent: boolean;
  averageGap: number;
  roundedGap: number;
  variance: number;
} {
  if (gaps.length === 0) {
    return { isConsistent: true, averageGap: 0, roundedGap: 0, variance: 0 };
  }

  if (gaps.length === 1) {
    const rounded = roundValue(gaps[0]);
    return { isConsistent: true, averageGap: gaps[0], roundedGap: rounded, variance: 0 };
  }

  // Calculate average
  const avg = gaps.reduce((a, b) => a + b, 0) / gaps.length;

  // Calculate variance
  const variance = gaps.reduce((sum, gap) => sum + Math.pow(gap - avg, 2), 0) / gaps.length;
  const stdDev = Math.sqrt(variance);

  // Determine consistency: standard deviation less than specified percentage of average
  const tolerance = avg * (tolerancePercent / 100);
  const isConsistent = stdDev <= tolerance;

  // Round to integer
  const roundedGap = roundValue(avg);

  return { isConsistent, averageGap: avg, roundedGap, variance };
}

/**
 * Round gap to common values
 * Common values: 0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64
 */
export function roundToCommonGap(gap: number): number {
  const COMMON_GAPS = [0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96, 128];

  // Find closest common value
  let closest = COMMON_GAPS[0];
  let minDiff = Math.abs(gap - closest);

  for (const commonGap of COMMON_GAPS) {
    const diff = Math.abs(gap - commonGap);
    if (diff < minDiff) {
      minDiff = diff;
      closest = commonGap;
    }
  }

  // If difference is too large (over 4px), use rounded value
  if (minDiff > 4) {
    return roundValue(gap);
  }

  return closest;
}

// ==================== Export Info Optimization ====================

/**
 * Optimize exportInfo, omit nodeId if it's the same as node id
 */
export function optimizeExportInfo(
  nodeId: string,
  exportInfo: { type: string; format: string; nodeId?: string; fileName?: string },
): { type: string; format: string; nodeId?: string; fileName?: string } {
  const result = { ...exportInfo };

  // If nodeId is the same as node id, omit it
  if (result.nodeId === nodeId) {
    delete result.nodeId;
  }

  return result;
}

```

--------------------------------------------------------------------------------
/src/services/cache/lru-cache.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * LRU (Least Recently Used) Memory Cache
 *
 * A generic in-memory cache with LRU eviction policy.
 * Used as a fast cache layer before disk cache.
 *
 * Features:
 * - O(1) get/set operations
 * - Automatic eviction of least recently used items
 * - Optional TTL (time-to-live) per item
 * - Size limiting by item count
 * - Statistics tracking
 *
 * @module services/cache/lru-cache
 */

import type { NodeCacheEntry } from "./types.js";

export interface LRUCacheConfig {
  /** Maximum number of items in cache */
  maxSize: number;
  /** Default TTL in milliseconds (0 = no expiration) */
  defaultTTL: number;
}

interface CacheEntry<T> {
  value: T;
  createdAt: number;
  expiresAt: number | null;
  size?: number;
}

export interface CacheStats {
  hits: number;
  misses: number;
  evictions: number;
  size: number;
  maxSize: number;
}

const DEFAULT_CONFIG: LRUCacheConfig = {
  maxSize: 100,
  defaultTTL: 0, // No expiration by default
};

/**
 * Generic LRU Cache implementation using Map
 * Map maintains insertion order, making it ideal for LRU
 */
export class LRUCache<T> {
  private cache: Map<string, CacheEntry<T>>;
  private config: LRUCacheConfig;
  private stats: CacheStats;

  constructor(config: Partial<LRUCacheConfig> = {}) {
    this.config = { ...DEFAULT_CONFIG, ...config };
    this.cache = new Map();
    this.stats = {
      hits: 0,
      misses: 0,
      evictions: 0,
      size: 0,
      maxSize: this.config.maxSize,
    };
  }

  /**
   * Get an item from cache
   * Returns null if not found or expired
   */
  get(key: string): T | null {
    const entry = this.cache.get(key);

    if (!entry) {
      this.stats.misses++;
      return null;
    }

    // Check expiration
    if (entry.expiresAt && Date.now() > entry.expiresAt) {
      this.delete(key);
      this.stats.misses++;
      return null;
    }

    // Move to end (most recently used)
    this.cache.delete(key);
    this.cache.set(key, entry);

    this.stats.hits++;
    return entry.value;
  }

  /**
   * Set an item in cache
   * @param key Cache key
   * @param value Value to cache
   * @param ttl Optional TTL in milliseconds (overrides default)
   */
  set(key: string, value: T, ttl?: number): void {
    // If key exists, delete it first (to update position)
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else {
      // Evict if at capacity
      while (this.cache.size >= this.config.maxSize) {
        this.evictOldest();
      }
    }

    const effectiveTTL = ttl ?? this.config.defaultTTL;
    const entry: CacheEntry<T> = {
      value,
      createdAt: Date.now(),
      expiresAt: effectiveTTL > 0 ? Date.now() + effectiveTTL : null,
    };

    this.cache.set(key, entry);
    this.stats.size = this.cache.size;
  }

  /**
   * Check if key exists (without updating access time)
   */
  has(key: string): boolean {
    const entry = this.cache.get(key);
    if (!entry) return false;

    // Check expiration
    if (entry.expiresAt && Date.now() > entry.expiresAt) {
      this.delete(key);
      return false;
    }

    return true;
  }

  /**
   * Delete an item from cache
   */
  delete(key: string): boolean {
    const existed = this.cache.delete(key);
    if (existed) {
      this.stats.size = this.cache.size;
    }
    return existed;
  }

  /**
   * Clear all items from cache
   */
  clear(): void {
    this.cache.clear();
    this.stats.size = 0;
  }

  /**
   * Get all keys in cache (most recent last)
   */
  keys(): string[] {
    return Array.from(this.cache.keys());
  }

  /**
   * Get cache size
   */
  get size(): number {
    return this.cache.size;
  }

  /**
   * Get cache statistics
   */
  getStats(): CacheStats {
    return { ...this.stats };
  }

  /**
   * Reset statistics
   */
  resetStats(): void {
    this.stats.hits = 0;
    this.stats.misses = 0;
    this.stats.evictions = 0;
  }

  /**
   * Get hit rate (0-1)
   */
  getHitRate(): number {
    const total = this.stats.hits + this.stats.misses;
    return total === 0 ? 0 : this.stats.hits / total;
  }

  /**
   * Clean expired entries
   */
  cleanExpired(): number {
    const now = Date.now();
    let cleaned = 0;

    for (const [key, entry] of this.cache.entries()) {
      if (entry.expiresAt && now > entry.expiresAt) {
        this.cache.delete(key);
        cleaned++;
      }
    }

    this.stats.size = this.cache.size;
    return cleaned;
  }

  /**
   * Evict the oldest (least recently used) item
   */
  private evictOldest(): void {
    const oldestKey = this.cache.keys().next().value;
    if (oldestKey !== undefined) {
      this.cache.delete(oldestKey);
      this.stats.evictions++;
      this.stats.size = this.cache.size;
    }
  }

  /**
   * Peek at an item without updating access time
   */
  peek(key: string): T | null {
    const entry = this.cache.get(key);
    if (!entry) return null;

    if (entry.expiresAt && Date.now() > entry.expiresAt) {
      return null;
    }

    return entry.value;
  }

  /**
   * Update config (applies to new entries only)
   */
  updateConfig(config: Partial<LRUCacheConfig>): void {
    this.config = { ...this.config, ...config };
    this.stats.maxSize = this.config.maxSize;

    // Evict if new maxSize is smaller
    while (this.cache.size > this.config.maxSize) {
      this.evictOldest();
    }
  }
}

/**
 * Specialized LRU cache for Figma node data
 * Includes version-aware caching
 */
export class NodeLRUCache extends LRUCache<NodeCacheEntry> {
  /**
   * Generate cache key for node data
   */
  static generateKey(fileKey: string, nodeId?: string, depth?: number): string {
    const parts = [fileKey];
    if (nodeId) parts.push(`n:${nodeId}`);
    if (depth !== undefined) parts.push(`d:${depth}`);
    return parts.join(":");
  }

  /**
   * Get node data with version check
   */
  getNode(fileKey: string, nodeId?: string, depth?: number, version?: string): unknown | null {
    const key = NodeLRUCache.generateKey(fileKey, nodeId, depth);
    const entry = this.get(key);

    if (!entry) return null;

    // Version mismatch - cache is stale
    if (version && entry.version && entry.version !== version) {
      this.delete(key);
      return null;
    }

    return entry.data;
  }

  /**
   * Set node data with metadata
   */
  setNode(
    data: unknown,
    fileKey: string,
    nodeId?: string,
    depth?: number,
    version?: string,
    ttl?: number,
  ): void {
    const key = NodeLRUCache.generateKey(fileKey, nodeId, depth);
    const entry: NodeCacheEntry = {
      data,
      fileKey,
      nodeId,
      version,
      depth,
    };
    this.set(key, entry, ttl);
  }

  /**
   * Invalidate all cache entries for a file
   */
  invalidateFile(fileKey: string): number {
    let invalidated = 0;
    for (const key of this.keys()) {
      if (key.startsWith(fileKey)) {
        this.delete(key);
        invalidated++;
      }
    }
    return invalidated;
  }

  /**
   * Invalidate cache entries for a specific node and its descendants
   */
  invalidateNode(fileKey: string, nodeId: string): number {
    let invalidated = 0;
    const prefix = NodeLRUCache.generateKey(fileKey, nodeId);
    for (const key of this.keys()) {
      if (key.startsWith(prefix)) {
        this.delete(key);
        invalidated++;
      }
    }
    return invalidated;
  }
}

```

--------------------------------------------------------------------------------
/src/services/cache/cache-manager.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Unified Cache Manager
 *
 * Manages multi-layer caching with L1 (memory) and L2 (disk) layers.
 *
 * Cache hierarchy:
 * - L1: In-memory LRU cache (fast, limited size)
 * - L2: Disk-based cache (persistent, larger capacity)
 *
 * @module services/cache/cache-manager
 */

import type { CacheConfig, CacheStatistics } from "./types.js";
import { DEFAULT_MEMORY_CONFIG } from "./types.js";
import { NodeLRUCache } from "./lru-cache.js";
import { DiskCache } from "./disk-cache.js";
import os from "os";
import path from "path";

/**
 * Default cache configuration
 */
const DEFAULT_CONFIG: CacheConfig = {
  enabled: true,
  memory: DEFAULT_MEMORY_CONFIG,
  disk: {
    cacheDir: path.join(os.homedir(), ".figma-mcp-cache"),
    maxSize: 500 * 1024 * 1024, // 500MB
    ttl: 24 * 60 * 60 * 1000, // 24 hours
  },
};

/**
 * Unified cache manager with multi-layer caching
 */
export class CacheManager {
  private config: CacheConfig;
  private memoryCache: NodeLRUCache;
  private diskCache: DiskCache | null;

  constructor(config: Partial<CacheConfig> = {}) {
    this.config = this.mergeConfig(DEFAULT_CONFIG, config);

    // Skip initialization if disabled
    if (!this.config.enabled) {
      this.memoryCache = new NodeLRUCache({ maxSize: 0, defaultTTL: 0 });
      this.diskCache = null;
      return;
    }

    // Initialize L1: Memory cache
    this.memoryCache = new NodeLRUCache({
      maxSize: this.config.memory.maxNodeItems,
      defaultTTL: this.config.memory.nodeTTL,
    });

    // Initialize L2: Disk cache
    this.diskCache = new DiskCache(this.config.disk);
  }

  /**
   * Deep merge configuration
   */
  private mergeConfig(defaults: CacheConfig, overrides: Partial<CacheConfig>): CacheConfig {
    return {
      enabled: overrides.enabled ?? defaults.enabled,
      memory: {
        ...defaults.memory,
        ...overrides.memory,
      },
      disk: {
        ...defaults.disk,
        ...overrides.disk,
      },
    };
  }

  // ==================== Node Data Operations ====================

  /**
   * Get node data with multi-layer cache lookup
   *
   * Flow: L1 (memory) -> L2 (disk) -> null (cache miss)
   */
  async getNodeData<T>(
    fileKey: string,
    nodeId?: string,
    depth?: number,
    version?: string,
  ): Promise<T | null> {
    if (!this.config.enabled) return null;

    // L1: Check memory cache
    const memoryData = this.memoryCache.getNode(fileKey, nodeId, depth, version);
    if (memoryData !== null) {
      return memoryData as T;
    }

    // L2: Check disk cache
    if (this.diskCache) {
      const diskData = await this.diskCache.get<T>(fileKey, nodeId, depth, version);
      if (diskData !== null) {
        // Backfill L1 cache
        this.memoryCache.setNode(diskData, fileKey, nodeId, depth, version);
        return diskData;
      }
    }

    return null;
  }

  /**
   * Set node data in both cache layers
   */
  async setNodeData<T>(
    data: T,
    fileKey: string,
    nodeId?: string,
    depth?: number,
    version?: string,
  ): Promise<void> {
    if (!this.config.enabled) return;

    // Write to L1 (memory)
    this.memoryCache.setNode(data, fileKey, nodeId, depth, version);

    // Write to L2 (disk)
    if (this.diskCache) {
      await this.diskCache.set(data, fileKey, nodeId, depth, version);
    }
  }

  /**
   * Check if node data exists in cache
   */
  async hasNodeData(fileKey: string, nodeId?: string, depth?: number): Promise<boolean> {
    if (!this.config.enabled) return false;

    const key = NodeLRUCache.generateKey(fileKey, nodeId, depth);
    if (this.memoryCache.has(key)) {
      return true;
    }

    if (this.diskCache) {
      return this.diskCache.has(fileKey, nodeId, depth);
    }

    return false;
  }

  // ==================== Image Operations ====================

  /**
   * Check if image is cached
   */
  async hasImage(fileKey: string, nodeId: string, format: string): Promise<string | null> {
    if (!this.config.enabled || !this.diskCache) return null;
    return this.diskCache.hasImage(fileKey, nodeId, format);
  }

  /**
   * Cache image file
   */
  async cacheImage(
    sourcePath: string,
    fileKey: string,
    nodeId: string,
    format: string,
  ): Promise<string> {
    if (!this.config.enabled || !this.diskCache) return sourcePath;
    return this.diskCache.cacheImage(sourcePath, fileKey, nodeId, format);
  }

  /**
   * Copy image from cache to target path
   */
  async copyImageFromCache(
    fileKey: string,
    nodeId: string,
    format: string,
    targetPath: string,
  ): Promise<boolean> {
    if (!this.config.enabled || !this.diskCache) return false;
    return this.diskCache.copyImageFromCache(fileKey, nodeId, format, targetPath);
  }

  // ==================== Invalidation Operations ====================

  /**
   * Invalidate all cache entries for a file
   */
  async invalidateFile(fileKey: string): Promise<{ memory: number; disk: number }> {
    const memoryInvalidated = this.memoryCache.invalidateFile(fileKey);
    const diskInvalidated = this.diskCache ? await this.diskCache.invalidateFile(fileKey) : 0;

    return { memory: memoryInvalidated, disk: diskInvalidated };
  }

  /**
   * Invalidate cache for a specific node
   */
  async invalidateNode(fileKey: string, nodeId: string): Promise<{ memory: number; disk: number }> {
    const memoryInvalidated = this.memoryCache.invalidateNode(fileKey, nodeId);
    const diskInvalidated = this.diskCache
      ? (await this.diskCache.delete(fileKey, nodeId))
        ? 1
        : 0
      : 0;

    return { memory: memoryInvalidated, disk: diskInvalidated };
  }

  // ==================== Maintenance Operations ====================

  /**
   * Clean expired cache entries from all layers
   */
  async cleanExpired(): Promise<{ memory: number; disk: number }> {
    const memoryCleaned = this.memoryCache.cleanExpired();
    const diskCleaned = this.diskCache ? await this.diskCache.cleanExpired() : 0;

    return { memory: memoryCleaned, disk: diskCleaned };
  }

  /**
   * Clear all cache
   */
  async clearAll(): Promise<void> {
    this.memoryCache.clear();
    if (this.diskCache) {
      await this.diskCache.clearAll();
    }
  }

  /**
   * Get combined cache statistics
   */
  async getStats(): Promise<CacheStatistics> {
    const memoryStats = this.memoryCache.getStats();

    if (!this.diskCache) {
      return {
        enabled: this.config.enabled,
        memory: {
          hits: memoryStats.hits,
          misses: memoryStats.misses,
          size: memoryStats.size,
          maxSize: memoryStats.maxSize,
          hitRate: this.memoryCache.getHitRate(),
          evictions: memoryStats.evictions,
        },
        disk: {
          hits: 0,
          misses: 0,
          totalSize: 0,
          maxSize: this.config.disk.maxSize,
          nodeFileCount: 0,
          imageFileCount: 0,
        },
      };
    }

    const diskStats = await this.diskCache.getStats();

    return {
      enabled: this.config.enabled,
      memory: {
        hits: memoryStats.hits,
        misses: memoryStats.misses,
        size: memoryStats.size,
        maxSize: memoryStats.maxSize,
        hitRate: this.memoryCache.getHitRate(),
        evictions: memoryStats.evictions,
      },
      disk: diskStats,
    };
  }

  /**
   * Get cache directory path
   */
  getCacheDir(): string {
    return this.config.disk.cacheDir;
  }

  /**
   * Check if caching is enabled
   */
  isEnabled(): boolean {
    return this.config.enabled;
  }

  /**
   * Reset statistics
   */
  resetStats(): void {
    this.memoryCache.resetStats();
  }
}

// Export singleton instance
export const cacheManager = new CacheManager();

```

--------------------------------------------------------------------------------
/src/core/layout.ts:
--------------------------------------------------------------------------------

```typescript
import { isFrame, isLayout, isRectangle } from "~/utils/validation.js";
import type {
  Node as FigmaDocumentNode,
  HasFramePropertiesTrait,
  HasLayoutTrait,
} from "@figma/rest-api-spec";
import { generateCSSShorthand } from "~/utils/css.js";

export interface SimplifiedLayout {
  mode: "none" | "row" | "column";
  justifyContent?: "flex-start" | "flex-end" | "center" | "space-between" | "baseline" | "stretch";
  alignItems?: "flex-start" | "flex-end" | "center" | "space-between" | "baseline" | "stretch";
  alignSelf?: "flex-start" | "flex-end" | "center" | "stretch";
  wrap?: boolean;
  gap?: string;
  locationRelativeToParent?: {
    x: number;
    y: number;
  };
  dimensions?: {
    width?: number;
    height?: number;
    aspectRatio?: number;
  };
  padding?: string;
  sizing?: {
    horizontal?: "fixed" | "fill" | "hug";
    vertical?: "fixed" | "fill" | "hug";
  };
  overflowScroll?: ("x" | "y")[];
  position?: "absolute";
}

// Convert Figma's layout config into a more typical flex-like schema
export function buildSimplifiedLayout(
  n: FigmaDocumentNode,
  parent?: FigmaDocumentNode,
): SimplifiedLayout {
  const frameValues = buildSimplifiedFrameValues(n);
  const layoutValues = buildSimplifiedLayoutValues(n, parent, frameValues.mode) || {};

  return { ...frameValues, ...layoutValues };
}

/**
 * Convert Figma's primaryAxisAlignItems to CSS justifyContent
 * Primary axis: horizontal for row, vertical for column
 */
function convertJustifyContent(
  axisAlign?: HasFramePropertiesTrait["primaryAxisAlignItems"],
  stretch?: {
    children: FigmaDocumentNode[];
    mode: "row" | "column";
  },
) {
  // Check if all children fill the main axis (stretch behavior)
  if (stretch) {
    const { children, mode } = stretch;
    const shouldStretch =
      children.length > 0 &&
      children.every((c) => {
        if ("layoutPositioning" in c && c.layoutPositioning === "ABSOLUTE") return true;
        // Primary axis: horizontal for row, vertical for column
        if (mode === "row") {
          return "layoutSizingHorizontal" in c && c.layoutSizingHorizontal === "FILL";
        } else {
          return "layoutSizingVertical" in c && c.layoutSizingVertical === "FILL";
        }
      });

    if (shouldStretch) return "stretch";
  }

  switch (axisAlign) {
    case "MIN":
      // MIN, AKA flex-start, is the default alignment
      return undefined;
    case "MAX":
      return "flex-end";
    case "CENTER":
      return "center";
    case "SPACE_BETWEEN":
      return "space-between";
    default:
      return undefined;
  }
}

/**
 * Convert Figma's counterAxisAlignItems to CSS alignItems
 * Counter axis: vertical for row, horizontal for column
 * Note: SPACE_BETWEEN is not valid for counter axis in Figma
 */
function convertAlignItems(
  axisAlign?: HasFramePropertiesTrait["counterAxisAlignItems"],
  stretch?: {
    children: FigmaDocumentNode[];
    mode: "row" | "column";
  },
) {
  // Check if all children fill the cross axis (stretch behavior)
  if (stretch) {
    const { children, mode } = stretch;
    const shouldStretch =
      children.length > 0 &&
      children.every((c) => {
        if ("layoutPositioning" in c && c.layoutPositioning === "ABSOLUTE") return true;
        // Counter axis: vertical for row, horizontal for column
        if (mode === "row") {
          return "layoutSizingVertical" in c && c.layoutSizingVertical === "FILL";
        } else {
          return "layoutSizingHorizontal" in c && c.layoutSizingHorizontal === "FILL";
        }
      });

    if (shouldStretch) return "stretch";
  }

  switch (axisAlign) {
    case "MIN":
      // MIN, AKA flex-start, is the default alignment
      return undefined;
    case "MAX":
      return "flex-end";
    case "CENTER":
      return "center";
    case "BASELINE":
      return "baseline";
    default:
      return undefined;
  }
}

function convertSelfAlign(align?: HasLayoutTrait["layoutAlign"]) {
  switch (align) {
    case "MIN":
      // MIN, AKA flex-start, is the default alignment
      return undefined;
    case "MAX":
      return "flex-end";
    case "CENTER":
      return "center";
    case "STRETCH":
      return "stretch";
    default:
      return undefined;
  }
}

// interpret sizing
function convertSizing(
  s?: HasLayoutTrait["layoutSizingHorizontal"] | HasLayoutTrait["layoutSizingVertical"],
) {
  if (s === "FIXED") return "fixed";
  if (s === "FILL") return "fill";
  if (s === "HUG") return "hug";
  return undefined;
}

function buildSimplifiedFrameValues(n: FigmaDocumentNode): SimplifiedLayout | { mode: "none" } {
  if (!isFrame(n)) {
    return { mode: "none" };
  }

  const frameValues: SimplifiedLayout = {
    mode:
      !n.layoutMode || n.layoutMode === "NONE"
        ? "none"
        : n.layoutMode === "HORIZONTAL"
          ? "row"
          : "column",
  };

  const overflowScroll: SimplifiedLayout["overflowScroll"] = [];
  if (n.overflowDirection?.includes("HORIZONTAL")) overflowScroll.push("x");
  if (n.overflowDirection?.includes("VERTICAL")) overflowScroll.push("y");
  if (overflowScroll.length > 0) frameValues.overflowScroll = overflowScroll;

  if (frameValues.mode === "none") {
    return frameValues;
  }

  // Convert Figma alignment to CSS flex properties
  frameValues.justifyContent = convertJustifyContent(n.primaryAxisAlignItems ?? "MIN", {
    children: n.children,
    mode: frameValues.mode,
  });
  frameValues.alignItems = convertAlignItems(n.counterAxisAlignItems ?? "MIN", {
    children: n.children,
    mode: frameValues.mode,
  });
  frameValues.alignSelf = convertSelfAlign(n.layoutAlign);

  // Only include wrap if it's set to WRAP, since flex layouts don't default to wrapping
  frameValues.wrap = n.layoutWrap === "WRAP" ? true : undefined;
  frameValues.gap = n.itemSpacing ? `${n.itemSpacing ?? 0}px` : undefined;
  // gather padding
  if (n.paddingTop || n.paddingBottom || n.paddingLeft || n.paddingRight) {
    frameValues.padding = generateCSSShorthand({
      top: n.paddingTop ?? 0,
      right: n.paddingRight ?? 0,
      bottom: n.paddingBottom ?? 0,
      left: n.paddingLeft ?? 0,
    });
  }

  return frameValues;
}

function buildSimplifiedLayoutValues(
  n: FigmaDocumentNode,
  parent: FigmaDocumentNode | undefined,
  mode: "row" | "column" | "none",
): SimplifiedLayout | undefined {
  if (!isLayout(n)) return undefined;

  const layoutValues: SimplifiedLayout = { mode };

  layoutValues.sizing = {
    horizontal: convertSizing(n.layoutSizingHorizontal),
    vertical: convertSizing(n.layoutSizingVertical),
  };

  // Only include positioning-related properties if parent layout isn't flex or if the node is absolute
  if (isFrame(parent) && (parent?.layoutMode === "NONE" || n.layoutPositioning === "ABSOLUTE")) {
    if (n.layoutPositioning === "ABSOLUTE") {
      layoutValues.position = "absolute";
    }
    if (n.absoluteBoundingBox && parent.absoluteBoundingBox) {
      layoutValues.locationRelativeToParent = {
        x: n.absoluteBoundingBox.x - (parent?.absoluteBoundingBox?.x ?? n.absoluteBoundingBox.x),
        y: n.absoluteBoundingBox.y - (parent?.absoluteBoundingBox?.y ?? n.absoluteBoundingBox.y),
      };
    }
    return layoutValues;
  }

  // Handle dimensions based on layout growth and alignment
  if (isRectangle("absoluteBoundingBox", n) && isRectangle("absoluteBoundingBox", parent)) {
    const dimensions: { width?: number; height?: number; aspectRatio?: number } = {};

    // Only include dimensions that aren't meant to stretch
    if (mode === "row") {
      if (!n.layoutGrow && n.layoutSizingHorizontal === "FIXED")
        dimensions.width = n.absoluteBoundingBox.width;
      if (n.layoutAlign !== "STRETCH" && n.layoutSizingVertical === "FIXED")
        dimensions.height = n.absoluteBoundingBox.height;
    } else if (mode === "column") {
      // column
      if (n.layoutAlign !== "STRETCH" && n.layoutSizingHorizontal === "FIXED")
        dimensions.width = n.absoluteBoundingBox.width;
      if (!n.layoutGrow && n.layoutSizingVertical === "FIXED")
        dimensions.height = n.absoluteBoundingBox.height;

      if (n.preserveRatio) {
        dimensions.aspectRatio = n.absoluteBoundingBox?.width / n.absoluteBoundingBox?.height;
      }
    }

    if (Object.keys(dimensions).length > 0) {
      layoutValues.dimensions = dimensions;
    }
  }

  return layoutValues;
}

```

--------------------------------------------------------------------------------
/tests/unit/algorithms/icon.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Icon Detection Algorithm Unit Tests
 *
 * Tests the icon detection algorithm for identifying and merging
 * vector layers that should be exported as single icons.
 */

import { describe, it, expect, beforeAll } from "vitest";
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import {
  detectIcon,
  analyzeNodeTree,
  DEFAULT_CONFIG,
  type FigmaNode,
  type IconDetectionResult,
} from "~/algorithms/icon/index.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fixturesPath = path.join(__dirname, "../../fixtures");

// Load test fixture
function loadTestData(): FigmaNode {
  const dataPath = path.join(fixturesPath, "figma-data/real-node-data.json");
  const rawData = JSON.parse(fs.readFileSync(dataPath, "utf-8"));
  const nodeKey = Object.keys(rawData.nodes)[0];
  return rawData.nodes[nodeKey].document;
}

// Count total icons detected
function countIcons(results: IconDetectionResult[]): number {
  return results.filter((r) => r.shouldMerge).length;
}

describe("Icon Detection Algorithm", () => {
  let testData: FigmaNode;

  beforeAll(() => {
    testData = loadTestData();
  });

  describe("Configuration", () => {
    it("should have sensible default configuration", () => {
      expect(DEFAULT_CONFIG.maxIconSize).toBe(300);
      expect(DEFAULT_CONFIG.minIconSize).toBe(8);
      expect(DEFAULT_CONFIG.mergeableRatio).toBe(0.6);
      expect(DEFAULT_CONFIG.maxDepth).toBe(5);
    });
  });

  describe("Size Constraints", () => {
    it("should reject nodes that are too large", () => {
      const largeNode: FigmaNode = {
        id: "large-1",
        name: "Large Node",
        type: "GROUP",
        absoluteBoundingBox: { x: 0, y: 0, width: 500, height: 500 },
        children: [{ id: "child-1", name: "Vector", type: "VECTOR" }],
      };

      const result = detectIcon(largeNode, DEFAULT_CONFIG);
      expect(result.shouldMerge).toBe(false);
      expect(result.reason).toContain("large");
    });

    it("should reject nodes that are too small", () => {
      const smallNode: FigmaNode = {
        id: "small-1",
        name: "Small Node",
        type: "GROUP",
        absoluteBoundingBox: { x: 0, y: 0, width: 4, height: 4 },
        children: [{ id: "child-1", name: "Vector", type: "VECTOR" }],
      };

      const result = detectIcon(smallNode, DEFAULT_CONFIG);
      expect(result.shouldMerge).toBe(false);
    });
  });

  describe("Node Type Detection", () => {
    it("should detect vector-only groups as icons", () => {
      const vectorGroup: FigmaNode = {
        id: "icon-1",
        name: "Search Icon",
        type: "GROUP",
        absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
        children: [
          { id: "v1", name: "Circle", type: "ELLIPSE" },
          { id: "v2", name: "Line", type: "LINE" },
        ],
      };

      const result = detectIcon(vectorGroup, DEFAULT_CONFIG);
      expect(result.shouldMerge).toBe(true);
      expect(result.exportFormat).toBe("SVG");
    });

    it("should reject groups containing TEXT nodes", () => {
      const textGroup: FigmaNode = {
        id: "text-group",
        name: "Button with Text",
        type: "GROUP",
        absoluteBoundingBox: { x: 0, y: 0, width: 100, height: 40 },
        children: [
          { id: "bg", name: "Background", type: "RECTANGLE" },
          { id: "label", name: "Label", type: "TEXT" },
        ],
      };

      const result = detectIcon(textGroup, DEFAULT_CONFIG);
      expect(result.shouldMerge).toBe(false);
      expect(result.reason).toContain("TEXT");
    });
  });

  describe("Export Format Selection", () => {
    it("should choose SVG for pure vector icons", () => {
      const vectorIcon: FigmaNode = {
        id: "svg-icon",
        name: "Star",
        type: "GROUP",
        absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
        children: [{ id: "star", name: "Star", type: "STAR" }],
      };

      const result = detectIcon(vectorIcon, DEFAULT_CONFIG);
      expect(result.shouldMerge).toBe(true);
      expect(result.exportFormat).toBe("SVG");
    });

    it("should choose PNG for icons with complex effects", () => {
      const effectIcon: FigmaNode = {
        id: "effect-icon",
        name: "Shadow Icon",
        type: "GROUP",
        absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
        effects: [{ type: "DROP_SHADOW", visible: true }],
        children: [{ id: "shape", name: "Shape", type: "RECTANGLE" }],
      };

      const result = detectIcon(effectIcon, DEFAULT_CONFIG);
      if (result.shouldMerge) {
        expect(result.exportFormat).toBe("PNG");
      }
    });

    it("should respect designer-specified export settings", () => {
      const exportNode: FigmaNode = {
        id: "export-icon",
        name: "Custom Export",
        type: "GROUP",
        absoluteBoundingBox: { x: 0, y: 0, width: 32, height: 32 },
        exportSettings: [{ format: "PNG", suffix: "", constraint: { type: "SCALE", value: 2 } }],
        children: [{ id: "v1", name: "Vector", type: "VECTOR" }],
      };

      const result = detectIcon(exportNode, DEFAULT_CONFIG);
      expect(result.shouldMerge).toBe(true);
      expect(result.exportFormat).toBe("PNG");
    });
  });

  describe("Mergeable Types", () => {
    const mergeableTypes = [
      "VECTOR",
      "ELLIPSE",
      "RECTANGLE",
      "STAR",
      "POLYGON",
      "LINE",
      "BOOLEAN_OPERATION",
    ];

    mergeableTypes.forEach((type) => {
      it(`should recognize ${type} as mergeable`, () => {
        const node: FigmaNode = {
          id: `${type.toLowerCase()}-icon`,
          name: `${type} Icon`,
          type: "GROUP",
          absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
          children: [{ id: "child", name: type, type: type }],
        };

        const result = detectIcon(node, DEFAULT_CONFIG);
        expect(result.shouldMerge).toBe(true);
      });
    });
  });

  describe("Real Figma Data", () => {
    it("should load and parse real Figma data", () => {
      expect(testData).toBeDefined();
      expect(testData.type).toBe("GROUP");
    });

    it("should analyze entire node tree", () => {
      const result = analyzeNodeTree(testData, DEFAULT_CONFIG);
      expect(result).toHaveProperty("processedTree");
      expect(result).toHaveProperty("exportableIcons");
      expect(result).toHaveProperty("summary");
      expect(Array.isArray(result.exportableIcons)).toBe(true);
    });

    it("should detect appropriate number of icons", () => {
      const result = analyzeNodeTree(testData, DEFAULT_CONFIG);
      const iconCount = countIcons(result.exportableIcons);

      // Should detect some icons but not too many (avoid fragmentation)
      expect(iconCount).toBeGreaterThanOrEqual(0);
      expect(iconCount).toBeLessThan(10); // Should be merged, not fragmented
    });

    it("should not mark root node as icon", () => {
      const result = detectIcon(testData, DEFAULT_CONFIG);
      expect(result.shouldMerge).toBe(false);
    });
  });

  describe("Edge Cases", () => {
    it("should handle nodes without children", () => {
      const leafNode: FigmaNode = {
        id: "leaf",
        name: "Single Vector",
        type: "VECTOR",
        absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
      };

      const result = detectIcon(leafNode, DEFAULT_CONFIG);
      expect(result).toBeDefined();
    });

    it("should handle nodes without bounding box", () => {
      const noBoundsNode: FigmaNode = {
        id: "no-bounds",
        name: "No Bounds",
        type: "GROUP",
        children: [],
      };

      const result = detectIcon(noBoundsNode, DEFAULT_CONFIG);
      expect(result.shouldMerge).toBe(false);
    });

    it("should handle deeply nested structures", () => {
      const deepNode: FigmaNode = {
        id: "deep",
        name: "Deep",
        type: "GROUP",
        absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
        children: [
          {
            id: "level1",
            name: "Level 1",
            type: "GROUP",
            children: [
              {
                id: "level2",
                name: "Level 2",
                type: "GROUP",
                children: [{ id: "vector", name: "Vector", type: "VECTOR" }],
              },
            ],
          },
        ],
      };

      const result = detectIcon(deepNode, DEFAULT_CONFIG);
      expect(result).toBeDefined();
    });
  });
});

```

--------------------------------------------------------------------------------
/tests/integration/parser.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Parser Integration Tests
 *
 * Tests the complete parsing pipeline from raw Figma API response
 * to simplified node structure.
 */

import { describe, it, expect, beforeAll } from "vitest";
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import { parseFigmaResponse } from "~/core/parser.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fixturesPath = path.join(__dirname, "../fixtures");

// Load fixtures
function loadRawData(): unknown {
  const dataPath = path.join(fixturesPath, "figma-data/real-node-data.json");
  return JSON.parse(fs.readFileSync(dataPath, "utf-8"));
}

function loadExpectedOutput(): unknown {
  const dataPath = path.join(fixturesPath, "expected/real-node-data-optimized.json");
  return JSON.parse(fs.readFileSync(dataPath, "utf-8"));
}

describe("Figma Response Parser", () => {
  let rawData: unknown;
  let expectedOutput: ReturnType<typeof loadExpectedOutput>;

  beforeAll(() => {
    rawData = loadRawData();
    expectedOutput = loadExpectedOutput();
  });

  describe("Basic Parsing", () => {
    it("should parse raw Figma response", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      expect(result).toBeDefined();
      expect(result.name).toBeDefined();
      expect(result.nodes).toBeDefined();
      expect(Array.isArray(result.nodes)).toBe(true);
    });

    it("should extract file metadata", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      expect(result.name).toBe("Vigilkids产品站");
      expect(result.lastModified).toBeDefined();
    });
  });

  describe("Node Structure", () => {
    it("should preserve node hierarchy", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      expect(result.nodes.length).toBeGreaterThan(0);

      const rootNode = result.nodes[0];
      expect(rootNode.id).toBeDefined();
      expect(rootNode.name).toBeDefined();
      expect(rootNode.type).toBeDefined();
    });

    it("should generate CSS styles", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      const rootNode = result.nodes[0];
      expect(rootNode.cssStyles).toBeDefined();
      expect(rootNode.cssStyles?.width).toBeDefined();
      expect(rootNode.cssStyles?.height).toBeDefined();
    });
  });

  describe("Data Compression", () => {
    it("should significantly reduce data size", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      const originalSize = Buffer.byteLength(JSON.stringify(rawData));
      const simplifiedSize = Buffer.byteLength(JSON.stringify(result));
      const compressionRate = ((originalSize - simplifiedSize) / originalSize) * 100;

      // Should achieve at least 70% compression
      expect(compressionRate).toBeGreaterThan(70);
    });
  });

  describe("CSS Style Generation", () => {
    it("should convert colors to hex format", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      // Find a node with background color
      const findNodeWithBg = (
        nodes: Array<{ cssStyles?: Record<string, unknown>; children?: unknown[] }>,
      ): Record<string, unknown> | null => {
        for (const node of nodes) {
          if (node.cssStyles?.backgroundColor) {
            return node.cssStyles;
          }
          if (node.children) {
            const found = findNodeWithBg(
              node.children as Array<{ cssStyles?: Record<string, unknown>; children?: unknown[] }>,
            );
            if (found) return found;
          }
        }
        return null;
      };

      const styles = findNodeWithBg(result.nodes);
      if (styles?.backgroundColor) {
        expect(styles.backgroundColor).toMatch(/^#[A-Fa-f0-9]{6}$/);
      }
    });

    it("should round pixel values to integers", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      const rootNode = result.nodes[0];
      const width = rootNode.cssStyles?.width as string;
      const height = rootNode.cssStyles?.height as string;

      // Should be integer pixel values
      expect(width).toMatch(/^\d+px$/);
      expect(height).toMatch(/^\d+px$/);
    });
  });

  describe("Layout Detection Integration", () => {
    it("should detect flex layouts in appropriate nodes", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      // Find nodes with flex properties
      const findFlexNode = (
        nodes: Array<{ cssStyles?: Record<string, unknown>; children?: unknown[] }>,
      ): Record<string, unknown> | null => {
        for (const node of nodes) {
          if (node.cssStyles?.display === "flex") {
            return node.cssStyles;
          }
          if (node.children) {
            const found = findFlexNode(
              node.children as Array<{ cssStyles?: Record<string, unknown>; children?: unknown[] }>,
            );
            if (found) return found;
          }
        }
        return null;
      };

      const flexStyles = findFlexNode(result.nodes);
      if (flexStyles) {
        expect(flexStyles.display).toBe("flex");
        expect(flexStyles.flexDirection).toBeDefined();
      }
    });
  });

  describe("Icon Detection Integration", () => {
    it("should mark icon nodes with exportInfo", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      // Find nodes with export info
      const findExportNode = (
        nodes: Array<{ exportInfo?: unknown; children?: unknown[] }>,
      ): unknown | null => {
        for (const node of nodes) {
          if (node.exportInfo) {
            return node.exportInfo;
          }
          if (node.children) {
            const found = findExportNode(
              node.children as Array<{ exportInfo?: unknown; children?: unknown[] }>,
            );
            if (found) return found;
          }
        }
        return null;
      };

      const exportInfo = findExportNode(result.nodes);
      if (exportInfo) {
        expect(exportInfo).toHaveProperty("type");
        expect(exportInfo).toHaveProperty("format");
        expect(exportInfo).toHaveProperty("fileName");
      }
    });
  });

  describe("Text Node Processing", () => {
    it("should extract text content", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      // Find text nodes
      const findTextNode = (
        nodes: Array<{ type?: string; text?: string; children?: unknown[] }>,
      ): { text?: string } | null => {
        for (const node of nodes) {
          if (node.type === "TEXT" && node.text) {
            return node;
          }
          if (node.children) {
            const found = findTextNode(
              node.children as Array<{ type?: string; text?: string; children?: unknown[] }>,
            );
            if (found) return found;
          }
        }
        return null;
      };

      const textNode = findTextNode(result.nodes);
      if (textNode) {
        expect(textNode.text).toBeDefined();
        expect(typeof textNode.text).toBe("string");
      }
    });

    it("should include font styles for text nodes", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      const findTextStyles = (
        nodes: Array<{ type?: string; cssStyles?: Record<string, unknown>; children?: unknown[] }>,
      ): Record<string, unknown> | null => {
        for (const node of nodes) {
          if (node.type === "TEXT" && node.cssStyles) {
            return node.cssStyles;
          }
          if (node.children) {
            const found = findTextStyles(
              node.children as Array<{
                type?: string;
                cssStyles?: Record<string, unknown>;
                children?: unknown[];
              }>,
            );
            if (found) return found;
          }
        }
        return null;
      };

      const textStyles = findTextStyles(result.nodes);
      if (textStyles) {
        expect(textStyles.fontFamily).toBeDefined();
        expect(textStyles.fontSize).toBeDefined();
      }
    });
  });

  describe("Output Stability", () => {
    it("should produce consistent output structure", () => {
      const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);

      // Compare key structure with expected output
      expect(Object.keys(result)).toEqual(Object.keys(expectedOutput as object));
      expect(result.nodes.length).toBe((expectedOutput as { nodes: unknown[] }).nodes.length);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/resources/figma-resources.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Figma Resources - Expose Figma data as MCP Resources
 * Resources are lightweight, on-demand data sources that save tokens
 */

import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { FigmaService } from "../services/figma.js";
import type { SimplifiedNode } from "../types/index.js";

// ==================== Types ====================

export interface FileMetadata {
  name: string;
  lastModified: string;
  version: string;
  pages: Array<{ id: string; name: string; childCount: number }>;
  thumbnailUrl?: string;
}

export interface StyleTokens {
  colors: Array<{ name: string; value: string; hex: string }>;
  typography: Array<{
    name: string;
    fontFamily: string;
    fontSize: number;
    fontWeight: number;
    lineHeight?: number;
  }>;
  effects: Array<{ name: string; type: string; value: string }>;
}

export interface ComponentSummary {
  id: string;
  name: string;
  description?: string;
  type: "COMPONENT" | "COMPONENT_SET";
  variants?: string[];
}

// ==================== Resource Handlers ====================

/**
 * Extract file metadata (lightweight, ~200 tokens)
 */
export async function getFileMetadata(
  figmaService: FigmaService,
  fileKey: string,
): Promise<FileMetadata> {
  const file = await figmaService.getFile(fileKey, 1); // depth=1 for minimal data

  const pages = file.nodes
    .filter((node) => node.type === "CANVAS")
    .map((page) => ({
      id: page.id,
      name: page.name,
      childCount: page.children?.length ?? 0,
    }));

  return {
    name: file.name,
    lastModified: file.lastModified,
    version: file.version ?? "",
    pages,
  };
}

/**
 * Extract style tokens from file (colors, typography, effects) (~500 tokens)
 */
export async function getStyleTokens(
  figmaService: FigmaService,
  fileKey: string,
): Promise<StyleTokens> {
  const file = await figmaService.getFile(fileKey, 3); // Need some depth for styles

  const colors: StyleTokens["colors"] = [];
  const typography: StyleTokens["typography"] = [];
  const effects: StyleTokens["effects"] = [];
  const seenColors = new Set<string>();
  const seenFonts = new Set<string>();

  function extractFromNode(node: SimplifiedNode) {
    // Extract colors from fills
    if (node.cssStyles) {
      const bgColor = node.cssStyles.background || node.cssStyles.backgroundColor;
      if (bgColor && !seenColors.has(bgColor)) {
        seenColors.add(bgColor);
        colors.push({
          name: node.name || "unnamed",
          value: bgColor,
          hex: bgColor,
        });
      }

      const textColor = node.cssStyles.color;
      if (textColor && !seenColors.has(textColor)) {
        seenColors.add(textColor);
        colors.push({
          name: `${node.name || "text"}-color`,
          value: textColor,
          hex: textColor,
        });
      }

      // Extract typography
      if (node.cssStyles.fontFamily && node.cssStyles.fontSize) {
        const fontKey = `${node.cssStyles.fontFamily}-${node.cssStyles.fontSize}-${node.cssStyles.fontWeight || 400}`;
        if (!seenFonts.has(fontKey)) {
          seenFonts.add(fontKey);
          typography.push({
            name: node.name || "text",
            fontFamily: node.cssStyles.fontFamily,
            fontSize: parseFloat(String(node.cssStyles.fontSize)) || 14,
            fontWeight: parseFloat(String(node.cssStyles.fontWeight)) || 400,
            lineHeight: node.cssStyles.lineHeight
              ? parseFloat(String(node.cssStyles.lineHeight))
              : undefined,
          });
        }
      }

      // Extract effects (shadows, blur)
      if (node.cssStyles.boxShadow) {
        effects.push({
          name: `${node.name || "element"}-shadow`,
          type: "shadow",
          value: String(node.cssStyles.boxShadow),
        });
      }
    }

    // Recurse into children
    if (node.children) {
      for (const child of node.children) {
        extractFromNode(child);
      }
    }
  }

  for (const node of file.nodes) {
    extractFromNode(node);
  }

  // Limit results to avoid token bloat
  return {
    colors: colors.slice(0, 20),
    typography: typography.slice(0, 10),
    effects: effects.slice(0, 10),
  };
}

/**
 * Extract component list (~300 tokens)
 */
export async function getComponentList(
  figmaService: FigmaService,
  fileKey: string,
): Promise<ComponentSummary[]> {
  const file = await figmaService.getFile(fileKey, 5); // Need depth for components
  const components: ComponentSummary[] = [];

  function findComponents(node: SimplifiedNode) {
    if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") {
      components.push({
        id: node.id,
        name: node.name,
        type: node.type as "COMPONENT" | "COMPONENT_SET",
        variants:
          node.type === "COMPONENT_SET" ? node.children?.map((c) => c.name).slice(0, 5) : undefined,
      });
    }

    if (node.children) {
      for (const child of node.children) {
        findComponents(child);
      }
    }
  }

  for (const node of file.nodes) {
    findComponents(node);
  }

  return components.slice(0, 50); // Limit to 50 components
}

/**
 * Extract images/assets list (~400 tokens)
 */
export async function getAssetList(
  figmaService: FigmaService,
  fileKey: string,
): Promise<
  Array<{
    nodeId: string;
    name: string;
    type: "vector" | "image" | "icon";
    exportFormats: string[];
    imageRef?: string;
  }>
> {
  const file = await figmaService.getFile(fileKey, 5);
  const assets: Array<{
    nodeId: string;
    name: string;
    type: "vector" | "image" | "icon";
    exportFormats: string[];
    imageRef?: string;
  }> = [];

  function findAssets(node: SimplifiedNode) {
    // Check for exportable assets (exportInfo is a single object, not array)
    if (node.exportInfo) {
      const isIcon =
        node.type === "VECTOR" ||
        node.type === "BOOLEAN_OPERATION" ||
        node.exportInfo.type === "IMAGE";

      assets.push({
        nodeId: node.id,
        name: node.name,
        type: isIcon ? "icon" : "vector",
        exportFormats: [node.exportInfo.format],
      });
    }

    // Check for image fills in fills array
    const imageFill = node.fills?.find(
      (fill): fill is { type: "IMAGE"; imageRef?: string } =>
        typeof fill === "object" && "type" in fill && fill.type === "IMAGE",
    );
    if (imageFill?.imageRef) {
      assets.push({
        nodeId: node.id,
        name: node.name,
        type: "image",
        exportFormats: ["png", "jpg"],
        imageRef: imageFill.imageRef,
      });
    }

    if (node.children) {
      for (const child of node.children) {
        findAssets(child);
      }
    }
  }

  for (const node of file.nodes) {
    findAssets(node);
  }

  return assets.slice(0, 100); // Limit to 100 assets
}

// ==================== Resource Templates ====================

/**
 * Create resource template for file metadata
 */
export function createFileMetadataTemplate(): ResourceTemplate {
  return new ResourceTemplate("figma://file/{fileKey}", {
    list: undefined, // Can't list all files without user's file list
    complete: {
      fileKey: async () => [], // Could be enhanced with recent files
    },
  });
}

/**
 * Create resource template for style tokens
 */
export function createStylesTemplate(): ResourceTemplate {
  return new ResourceTemplate("figma://file/{fileKey}/styles", {
    list: undefined,
    complete: {
      fileKey: async () => [],
    },
  });
}

/**
 * Create resource template for components
 */
export function createComponentsTemplate(): ResourceTemplate {
  return new ResourceTemplate("figma://file/{fileKey}/components", {
    list: undefined,
    complete: {
      fileKey: async () => [],
    },
  });
}

/**
 * Create resource template for assets
 */
export function createAssetsTemplate(): ResourceTemplate {
  return new ResourceTemplate("figma://file/{fileKey}/assets", {
    list: undefined,
    complete: {
      fileKey: async () => [],
    },
  });
}

// ==================== Help Content ====================

export const FIGMA_MCP_HELP = `# Figma MCP Server - Resource Guide

## Available Resources

### File Metadata
\`figma://file/{fileKey}\`
Returns: File name, pages, last modified date
Token cost: ~200

### Design Tokens (Styles)
\`figma://file/{fileKey}/styles\`
Returns: Colors, typography, effects extracted from file
Token cost: ~500

### Component List
\`figma://file/{fileKey}/components\`
Returns: All components and component sets with variants
Token cost: ~300

### Asset List
\`figma://file/{fileKey}/assets\`
Returns: Exportable images, icons, vectors with node IDs
Token cost: ~400

## How to Get fileKey

From Figma URL: \`figma.com/file/{fileKey}/...\`
Or: \`figma.com/design/{fileKey}/...\`

## Example Usage

1. Read file metadata: \`figma://file/abc123\`
2. Get color palette: \`figma://file/abc123/styles\`
3. List components: \`figma://file/abc123/components\`
4. Find assets to download: \`figma://file/abc123/assets\`

## Tools vs Resources

- **Resources**: Read-only data, user-controlled, lightweight
- **Tools**: Actions (download images), AI-controlled, heavier

Use Resources for exploration, Tools for execution.
`;

```

--------------------------------------------------------------------------------
/docs/en/absolute-to-relative-research.md:
--------------------------------------------------------------------------------

```markdown
# Absolute Position to Margin/Padding Conversion Research

## Research Overview

This document summarizes industry implementations and academic research on converting absolute positioning (`position: absolute` + `left/top`) to relative layouts (`margin`, `padding`, `gap`).

## 1. Industry Implementations

### 1.1 FigmaToCode (bernaferrari/FigmaToCode)

**Approach**: AltNodes Intermediate Representation

**Key Points**:

- Uses a 4-stage transformation pipeline:

  1. Node Conversion - Figma nodes → JSON with optimizations
  2. Intermediate Representation - JSON → AltNodes (virtual DOM)
  3. Layout Optimization - Detect auto-layouts, responsive constraints
  4. Code Generation - Framework-specific output

- For complex layouts (absolute + auto-layout), makes "intelligent decisions about structure"
- Detects parent-child relationships and z-index ordering
- Uses `insets` for best cases, `left/top` for worst cases

**Source**: https://github.com/bernaferrari/FigmaToCode

### 1.2 Facebook Yoga Layout Engine

**Approach**: CSS Flexbox Implementation in C++

**Padding/Margin Calculation**:

```
paddingAndBorderForAxis = leadingPaddingAndBorder + trailingPaddingAndBorder
marginForAxis = leadingMargin + trailingMargin
```

**Resolution Algorithm**:

- UnitPoint: Direct pixel value
- UnitPercent: `value * parentSize / 100`
- UnitAuto: Returns 0 for margins

**Key Functions**:

- `nodeLeadingPadding()` - Leading edge padding
- `nodeTrailingPadding()` - Trailing edge padding
- `nodeMarginForAxis()` - Total margin for axis
- `resolveValue()` - Unit conversion

**Source**: https://github.com/facebook/yoga

### 1.3 imgcook (Alibaba)

**Approach**: Rule-based + CV-based Layout Algorithm

**Key Points**:

- Converts design layers to flat JSON with absolute positions
- Uses rule-based algorithms to merge adjacent rows/blocks
- CV-based approach for generalization (pixel-level comparison)
- No specific formulas disclosed for margin/padding calculation

**Limitation**: Implementation details not publicly available

**Source**: https://www.alibabacloud.com/blog/imgcook-3-0-series-layout-algorithm-design-based-code-generation_597856

### 1.4 teleportHQ UIDL

**Approach**: Abstract UI Description Language

**Key Points**:

- CSS-like style properties in UIDL
- Design tokens for spacing constants (→ CSS variables)
- Does not appear to handle coordinate-based conversion

**Source**: https://github.com/teleporthq/teleport-code-generators

## 2. Academic Research

### 2.1 Layout Inference Algorithm for GUIs

**Paper**: "A layout inference algorithm for Graphical User Interfaces" (2015)

**Approach**: Two-phase algorithm using Allen's Interval Algebra

**Phase 1: Coordinate → Relative Positioning**

- Change coordinate-based positioning to relative positioning
- Use directed graphs and Allen relations
- Build spatial relationships between elements

**Phase 2: Pattern Matching & Graph Rewriting**

- Apply exploratory algorithm
- Pattern matching to identify layout structures
- Graph rewriting to obtain layout solutions

**Results**:

- 97% faithful to original views
- 84% maintain proportions when resized

**Source**: https://www.sciencedirect.com/science/article/abs/pii/S0950584915001718

### 2.2 Allen's Interval Algebra

**13 Basic Relations** (applicable to spatial layout):

| Relation      | Symbol | Description                                     |
| ------------- | ------ | ----------------------------------------------- |
| Precedes      | p      | A ends before B starts (gap between)            |
| Meets         | m      | A ends exactly where B starts                   |
| Overlaps      | o      | A starts before B, they overlap, B ends after A |
| Finished-by   | F      | B starts during A, ends together                |
| Contains      | D      | A completely contains B                         |
| Starts        | s      | A and B start together, A ends first            |
| Equals        | e      | A and B identical                               |
| Started-by    | S      | B starts when A starts, B ends after            |
| During        | d      | B completely contains A                         |
| Finishes      | f      | A starts during B, ends together                |
| Overlapped-by | O      | B starts before A, they overlap                 |
| Met-by        | M      | A starts exactly where B ends                   |
| Preceded-by   | P      | B ends before A starts (gap between)            |

**Application**: Determine spatial relationships to infer layout structure

**Source**: https://ics.uci.edu/~alspaugh/cls/shr/allen.html

## 3. Key Insights

### 3.1 Padding Inference Formula

When a parent container has children, padding can be inferred:

```
paddingTop = firstChild.y - parent.y
paddingLeft = firstChild.x - parent.x
paddingBottom = (parent.y + parent.height) - (lastChild.y + lastChild.height)
paddingRight = (parent.x + parent.width) - (lastChild.x + lastChild.width)
```

**For Row Layout** (flex-direction: row):

- Sort children by X position
- `paddingLeft` = first child's left offset from parent
- `paddingTop` = minimum top offset among children
- `gap` = consistent spacing between children (already implemented)

**For Column Layout** (flex-direction: column):

- Sort children by Y position
- `paddingTop` = first child's top offset from parent
- `paddingLeft` = minimum left offset among children
- `gap` = consistent spacing between children (already implemented)

### 3.2 Individual Margin Calculation

For elements that don't align perfectly with the primary axis:

```
For Row Layout:
  expectedY = parent.y + paddingTop
  marginTop = child.y - expectedY

For Column Layout:
  expectedX = parent.x + paddingLeft
  marginLeft = child.x - expectedX
```

### 3.3 Cross-Axis Alignment vs Margin

When elements have different cross-axis positions:

**Option A: Use align-items + individual margins**

```css
.parent {
  display: flex;
  align-items: flex-start;
}
.child-offset {
  margin-top: 10px; /* Individual offset */
}
```

**Option B: Use align-items: center/stretch**
If all elements are centered or stretched, no individual margins needed.

### 3.4 Absolute Position Preservation

Some elements MUST keep absolute positioning:

- Overlapping elements (IoU > 0.1)
- Stacked elements (z-index layering)
- Elements outside parent bounds
- Decorative/background elements

## 4. Proposed Algorithm

### Step 1: Classify Elements

```
For each parent with children:
  1. Detect overlapping elements (IoU > 0.1) → Keep absolute
  2. Remaining elements → Flow elements
```

### Step 2: Detect Layout Direction

```
For flow elements:
  1. Analyze horizontal vs vertical distribution
  2. Determine primary axis (row or column)
  3. Calculate gap consistency
```

### Step 3: Calculate Padding

```
If layout detected:
  1. Sort children by primary axis position
  2. paddingStart = first child offset from parent start
  3. paddingEnd = parent end - last child end
  4. Analyze cross-axis for paddingCross
```

### Step 4: Calculate Individual Margins

```
For each flow child:
  1. expectedPosition = based on padding + gap + previous elements
  2. actualPosition = child's current position
  3. If difference > threshold:
     - Add margin to child
```

### Step 5: Clean Up Styles

```
For flow children:
  1. Remove position: absolute
  2. Remove left, top
  3. Add margin if calculated

For parent:
  1. Add padding
  2. Keep gap (already implemented)
  3. Keep display: flex/grid
```

## 5. Implementation Considerations

### 5.1 Edge Cases

1. **Negative margins**: When elements overlap slightly
2. **Mixed alignments**: Some elements centered, others not
3. **Variable gaps**: Elements with inconsistent spacing
4. **Percentage values**: May need to convert px to %

### 5.2 Thresholds

| Parameter             | Recommended Value | Source   |
| --------------------- | ----------------- | -------- |
| Padding detection     | >= 0px            | Standard |
| Gap consistency CV    | <= 20%            | imgcook  |
| Alignment tolerance   | 2px               | Common   |
| Overlap IoU threshold | 0.1               | imgcook  |

### 5.3 Priority Order

1. Grid detection (highest priority for regular grids)
2. Flex detection with padding inference
3. Fall back to absolute positioning

## 6. References

1. **FigmaToCode**: https://github.com/bernaferrari/FigmaToCode
2. **Facebook Yoga**: https://github.com/facebook/yoga
3. **imgcook Blog**: https://www.alibabacloud.com/blog/imgcook-3-0-series-layout-algorithm-design-based-code-generation_597856
4. **Allen's Interval Algebra**: https://en.wikipedia.org/wiki/Allen's_interval_algebra
5. **Layout Inference Paper**: https://www.sciencedirect.com/science/article/abs/pii/S0950584915001718
6. **teleportHQ**: https://github.com/teleporthq/teleport-code-generators
7. **Yoga Go Port**: https://github.com/kjk/flex

## 7. Conclusion

The key insight from industry implementations is that converting absolute positioning to relative layouts requires:

1. **Spatial Analysis**: Use Allen's Interval Algebra or similar to understand element relationships
2. **Padding Inference**: Calculate parent padding from first/last child offsets
3. **Margin Calculation**: Handle individual element offsets that don't fit the primary layout
4. **Selective Preservation**: Keep absolute positioning for genuinely overlapping elements

The algorithm should be conservative - only convert when confident about the layout structure, otherwise preserve absolute positioning for accuracy.

```

--------------------------------------------------------------------------------
/tests/fixtures/expected/real-node-data-optimized.json:
--------------------------------------------------------------------------------

```json
{
  "name": "Vigilkids产品站",
  "lastModified": "2025-12-05T09:47:37Z",
  "thumbnailUrl": "https://s3-alpha.figma.com/thumbnails/9a38a8e4-5a00-4c07-a053-e71c7103a167?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAQ4GOSFWCXJW6HYPC%2F20251204%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251204T000000Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=dab594780137ae82433ff7205024276c20a3abb73855f2e393c7e6373086b892",
  "nodes": [
    {
      "id": "2:674",
      "name": "Group 1410104851",
      "type": "GROUP",
      "cssStyles": {
        "width": "1580px",
        "height": "895px",
        "position": "absolute",
        "left": "406px",
        "top": "422px"
      },
      "children": [
        {
          "id": "2:675",
          "name": "Rectangle 34",
          "type": "RECTANGLE",
          "cssStyles": {
            "width": "1580px",
            "height": "895px",
            "position": "absolute",
            "left": "0px",
            "top": "0px",
            "backgroundColor": "#FFFFFF",
            "borderRadius": "12px"
          }
        },
        {
          "id": "2:676",
          "name": "Group 1410104850",
          "type": "GROUP",
          "cssStyles": {
            "width": "350px",
            "height": "316px",
            "position": "absolute",
            "left": "615px",
            "top": "232px",
            "display": "flex",
            "flexDirection": "column",
            "gap": "32px",
            "justifyContent": "space-between",
            "alignItems": "center"
          },
          "children": [
            {
              "id": "2:689",
              "name": "Group 1410104849",
              "type": "GROUP",
              "cssStyles": {
                "width": "220px",
                "height": "138px",
                "position": "absolute",
                "left": "65px",
                "top": "0px"
              },
              "exportInfo": {
                "type": "IMAGE",
                "format": "PNG",
                "fileName": "group_1410104849.png"
              }
            },
            {
              "id": "2:677",
              "name": "Group 1410104480",
              "type": "GROUP",
              "cssStyles": {
                "width": "350px",
                "height": "148px",
                "position": "absolute",
                "left": "0px",
                "top": "168px",
                "display": "flex",
                "flexDirection": "column",
                "gap": "32px",
                "justifyContent": "space-between",
                "alignItems": "center"
              },
              "children": [
                {
                  "id": "2:678",
                  "name": "添加自定义关键词,当检测到关键词出现时您将接收警报",
                  "type": "TEXT",
                  "cssStyles": {
                    "width": "350px",
                    "height": "20px",
                    "position": "absolute",
                    "left": "0px",
                    "top": "0px",
                    "color": "#333333",
                    "fontFamily": "PingFang SC",
                    "fontSize": "14px",
                    "fontWeight": 500,
                    "textAlign": "center",
                    "verticalAlign": "middle",
                    "lineHeight": "20px"
                  },
                  "text": "添加自定义关键词,当检测到关键词出现时您将接收警报"
                },
                {
                  "id": "2:679",
                  "name": "Group 1410104479",
                  "type": "GROUP",
                  "cssStyles": {
                    "width": "240px",
                    "height": "98px",
                    "position": "absolute",
                    "left": "55px",
                    "top": "50px",
                    "display": "flex",
                    "flexDirection": "column",
                    "gap": "10px",
                    "justifyContent": "space-between",
                    "alignItems": "flex-start"
                  },
                  "children": [
                    {
                      "id": "2:680",
                      "name": "Group 1410086131",
                      "type": "GROUP",
                      "cssStyles": {
                        "width": "240px",
                        "height": "44px",
                        "position": "absolute",
                        "left": "0px",
                        "top": "0px"
                      },
                      "children": [
                        {
                          "id": "2:681",
                          "name": "Rectangle 34625783",
                          "type": "RECTANGLE",
                          "cssStyles": {
                            "width": "240px",
                            "height": "44px",
                            "position": "absolute",
                            "left": "0px",
                            "top": "0px",
                            "backgroundColor": "#24C790",
                            "borderRadius": "10px"
                          }
                        },
                        {
                          "id": "2:682",
                          "name": "Group 1410104509",
                          "type": "GROUP",
                          "cssStyles": {
                            "width": "115px",
                            "height": "20px",
                            "position": "absolute",
                            "left": "63px",
                            "top": "12px",
                            "display": "flex",
                            "gap": "10px",
                            "justifyContent": "space-between",
                            "alignItems": "flex-start"
                          },
                          "children": [
                            {
                              "id": "2:684",
                              "name": "Frame",
                              "type": "FRAME",
                              "cssStyles": {
                                "width": "20px",
                                "height": "20px",
                                "position": "absolute",
                                "left": "0px",
                                "top": "0px"
                              },
                              "exportInfo": {
                                "type": "IMAGE",
                                "format": "SVG",
                                "fileName": "frame.svg"
                              }
                            },
                            {
                              "id": "2:683",
                              "name": "AI生成关键词",
                              "type": "TEXT",
                              "cssStyles": {
                                "width": "84px",
                                "height": "16px",
                                "position": "absolute",
                                "left": "31px",
                                "top": "2px",
                                "color": "#FFFFFF",
                                "fontFamily": "Roboto",
                                "fontSize": "14px",
                                "fontWeight": 700,
                                "textAlign": "center",
                                "verticalAlign": "middle",
                                "lineHeight": "16px"
                              },
                              "text": "AI生成关键词"
                            }
                          ]
                        }
                      ]
                    },
                    {
                      "id": "2:686",
                      "name": "Group 1410104451",
                      "type": "GROUP",
                      "cssStyles": {
                        "width": "240px",
                        "height": "44px",
                        "position": "absolute",
                        "left": "0px",
                        "top": "54px",
                        "borderRadius": "10px"
                      },
                      "children": [
                        {
                          "id": "2:687",
                          "name": "Rectangle 34625783",
                          "type": "RECTANGLE",
                          "cssStyles": {
                            "width": "240px",
                            "height": "44px",
                            "position": "absolute",
                            "left": "0px",
                            "top": "0px",
                            "backgroundColor": "#FFFFFF",
                            "borderColor": "#C4C4C4",
                            "borderStyle": "solid",
                            "borderRadius": "10px"
                          }
                        },
                        {
                          "id": "2:688",
                          "name": "添加自定义关键词",
                          "type": "TEXT",
                          "cssStyles": {
                            "width": "146px",
                            "height": "16px",
                            "position": "absolute",
                            "left": "48px",
                            "top": "14px",
                            "color": "#333333",
                            "fontFamily": "Roboto",
                            "fontSize": "14px",
                            "fontWeight": 700,
                            "textAlign": "center",
                            "verticalAlign": "middle",
                            "lineHeight": "16px"
                          },
                          "text": "添加自定义关键词"
                        }
                      ]
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

```

--------------------------------------------------------------------------------
/tests/unit/algorithms/icon-optimization.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Icon Detection Optimization Tests
 *
 * Verifies that the optimized collectNodeStats() function produces
 * identical results to the original individual functions.
 */

import { describe, it, expect } from "vitest";

// Test the internal implementation
// We'll create mock nodes and verify the stats are correctly computed

interface MockNode {
  id: string;
  name: string;
  type: string;
  children?: MockNode[];
  absoluteBoundingBox?: { x: number; y: number; width: number; height: number };
  fills?: Array<{ type: string; visible?: boolean; imageRef?: string }>;
  effects?: Array<{ type: string; visible?: boolean }>;
}

// Helper functions to test (matching the original implementations)
const CONTAINER_TYPES = ["GROUP", "FRAME", "COMPONENT", "INSTANCE"];
const MERGEABLE_TYPES = [
  "VECTOR",
  "RECTANGLE",
  "ELLIPSE",
  "LINE",
  "POLYGON",
  "STAR",
  "BOOLEAN_OPERATION",
  "REGULAR_POLYGON",
];
const EXCLUDE_TYPES = ["TEXT", "COMPONENT", "INSTANCE"];
const PNG_REQUIRED_EFFECTS = ["DROP_SHADOW", "INNER_SHADOW", "LAYER_BLUR", "BACKGROUND_BLUR"];

function isContainerType(type: string): boolean {
  return CONTAINER_TYPES.includes(type);
}

function isMergeableType(type: string): boolean {
  return MERGEABLE_TYPES.includes(type);
}

function isExcludeType(type: string): boolean {
  return EXCLUDE_TYPES.includes(type);
}

function hasImageFill(node: MockNode): boolean {
  if (!node.fills) return false;
  return node.fills.some(
    (fill) => fill.type === "IMAGE" && fill.visible !== false && fill.imageRef,
  );
}

function hasComplexEffects(node: MockNode): boolean {
  if (!node.effects) return false;
  return node.effects.some(
    (effect) => effect.visible !== false && PNG_REQUIRED_EFFECTS.includes(effect.type),
  );
}

// Original functions (for comparison)
function calculateDepthOriginal(node: MockNode, currentDepth: number = 0): number {
  if (!node.children || node.children.length === 0) {
    return currentDepth;
  }
  return Math.max(...node.children.map((child) => calculateDepthOriginal(child, currentDepth + 1)));
}

function countTotalChildrenOriginal(node: MockNode): number {
  if (!node.children || node.children.length === 0) {
    return 0;
  }
  return node.children.reduce((sum, child) => sum + 1 + countTotalChildrenOriginal(child), 0);
}

function hasExcludeTypeInTreeOriginal(node: MockNode): boolean {
  if (isExcludeType(node.type)) {
    return true;
  }
  if (node.children) {
    return node.children.some((child) => hasExcludeTypeInTreeOriginal(child));
  }
  return false;
}

function hasImageFillInTreeOriginal(node: MockNode): boolean {
  if (hasImageFill(node)) {
    return true;
  }
  if (node.children) {
    return node.children.some((child) => hasImageFillInTreeOriginal(child));
  }
  return false;
}

function hasComplexEffectsInTreeOriginal(node: MockNode): boolean {
  if (hasComplexEffects(node)) {
    return true;
  }
  if (node.children) {
    return node.children.some((child) => hasComplexEffectsInTreeOriginal(child));
  }
  return false;
}

function areAllLeavesMergeableOriginal(node: MockNode): boolean {
  if (!node.children || node.children.length === 0) {
    return isMergeableType(node.type);
  }
  if (isContainerType(node.type)) {
    return node.children.every((child) => areAllLeavesMergeableOriginal(child));
  }
  return isMergeableType(node.type);
}

// Optimized single-pass function
interface NodeTreeStats {
  depth: number;
  totalChildren: number;
  hasExcludeType: boolean;
  hasImageFill: boolean;
  hasComplexEffects: boolean;
  allLeavesMergeable: boolean;
  mergeableRatio: number;
}

function collectNodeStats(node: MockNode): NodeTreeStats {
  if (!node.children || node.children.length === 0) {
    const isMergeable = isMergeableType(node.type);
    return {
      depth: 0,
      totalChildren: 0,
      hasExcludeType: isExcludeType(node.type),
      hasImageFill: hasImageFill(node),
      hasComplexEffects: hasComplexEffects(node),
      allLeavesMergeable: isMergeable,
      mergeableRatio: isMergeable ? 1 : 0,
    };
  }

  const childStats = node.children.map(collectNodeStats);
  const maxChildDepth = Math.max(...childStats.map((s) => s.depth));
  const totalDescendants = childStats.reduce((sum, s) => sum + 1 + s.totalChildren, 0);
  const hasExcludeInChildren = childStats.some((s) => s.hasExcludeType);
  const hasImageInChildren = childStats.some((s) => s.hasImageFill);
  const hasEffectsInChildren = childStats.some((s) => s.hasComplexEffects);
  const allChildrenMergeable = childStats.every((s) => s.allLeavesMergeable);

  const mergeableCount = node.children.filter(
    (child) => isMergeableType(child.type) || isContainerType(child.type),
  ).length;
  const mergeableRatio = mergeableCount / node.children.length;

  const allLeavesMergeable = isContainerType(node.type)
    ? allChildrenMergeable
    : isMergeableType(node.type);

  return {
    depth: maxChildDepth + 1,
    totalChildren: totalDescendants,
    hasExcludeType: isExcludeType(node.type) || hasExcludeInChildren,
    hasImageFill: hasImageFill(node) || hasImageInChildren,
    hasComplexEffects: hasComplexEffects(node) || hasEffectsInChildren,
    allLeavesMergeable,
    mergeableRatio,
  };
}

describe("Icon Detection Optimization", () => {
  describe("collectNodeStats equivalence", () => {
    const testCases: { name: string; node: MockNode }[] = [
      {
        name: "simple leaf node",
        node: {
          id: "1",
          name: "Vector",
          type: "VECTOR",
        },
      },
      {
        name: "leaf node with excludable type",
        node: {
          id: "1",
          name: "Text",
          type: "TEXT",
        },
      },
      {
        name: "container with vector children",
        node: {
          id: "1",
          name: "Group",
          type: "GROUP",
          children: [
            { id: "2", name: "Vector1", type: "VECTOR" },
            { id: "3", name: "Vector2", type: "VECTOR" },
          ],
        },
      },
      {
        name: "nested container",
        node: {
          id: "1",
          name: "Frame",
          type: "FRAME",
          children: [
            {
              id: "2",
              name: "Group",
              type: "GROUP",
              children: [
                { id: "3", name: "Ellipse", type: "ELLIPSE" },
                { id: "4", name: "Rect", type: "RECTANGLE" },
              ],
            },
          ],
        },
      },
      {
        name: "container with text child",
        node: {
          id: "1",
          name: "Button",
          type: "FRAME",
          children: [
            { id: "2", name: "BG", type: "RECTANGLE" },
            { id: "3", name: "Label", type: "TEXT" },
          ],
        },
      },
      {
        name: "node with image fill",
        node: {
          id: "1",
          name: "Image",
          type: "RECTANGLE",
          fills: [{ type: "IMAGE", visible: true, imageRef: "abc123" }],
        },
      },
      {
        name: "node with complex effects",
        node: {
          id: "1",
          name: "Shadow Box",
          type: "FRAME",
          effects: [{ type: "DROP_SHADOW", visible: true }],
          children: [{ id: "2", name: "Content", type: "RECTANGLE" }],
        },
      },
      {
        name: "deeply nested structure",
        node: {
          id: "1",
          name: "Root",
          type: "FRAME",
          children: [
            {
              id: "2",
              name: "Level1",
              type: "GROUP",
              children: [
                {
                  id: "3",
                  name: "Level2",
                  type: "GROUP",
                  children: [
                    {
                      id: "4",
                      name: "Level3",
                      type: "GROUP",
                      children: [{ id: "5", name: "Leaf", type: "VECTOR" }],
                    },
                  ],
                },
              ],
            },
          ],
        },
      },
    ];

    testCases.forEach(({ name, node }) => {
      it(`should produce equivalent results for: ${name}`, () => {
        const stats = collectNodeStats(node);

        // Compare with original functions
        expect(stats.depth).toBe(calculateDepthOriginal(node));
        expect(stats.totalChildren).toBe(countTotalChildrenOriginal(node));
        expect(stats.hasExcludeType).toBe(hasExcludeTypeInTreeOriginal(node));
        expect(stats.hasImageFill).toBe(hasImageFillInTreeOriginal(node));
        expect(stats.hasComplexEffects).toBe(hasComplexEffectsInTreeOriginal(node));
        expect(stats.allLeavesMergeable).toBe(areAllLeavesMergeableOriginal(node));
      });
    });
  });

  describe("edge cases", () => {
    it("should handle empty children array", () => {
      const node: MockNode = {
        id: "1",
        name: "Empty",
        type: "GROUP",
        children: [],
      };

      const stats = collectNodeStats(node);
      expect(stats.depth).toBe(0);
      expect(stats.totalChildren).toBe(0);
    });

    it("should handle invisible fills", () => {
      const node: MockNode = {
        id: "1",
        name: "Hidden Image",
        type: "RECTANGLE",
        fills: [{ type: "IMAGE", visible: false, imageRef: "abc123" }],
      };

      const stats = collectNodeStats(node);
      expect(stats.hasImageFill).toBe(false);
    });

    it("should handle invisible effects", () => {
      const node: MockNode = {
        id: "1",
        name: "Hidden Shadow",
        type: "RECTANGLE",
        effects: [{ type: "DROP_SHADOW", visible: false }],
      };

      const stats = collectNodeStats(node);
      expect(stats.hasComplexEffects).toBe(false);
    });

    it("should calculate correct mergeable ratio", () => {
      const node: MockNode = {
        id: "1",
        name: "Mixed",
        type: "FRAME",
        children: [
          { id: "2", name: "V1", type: "VECTOR" },
          { id: "3", name: "V2", type: "VECTOR" },
          { id: "4", name: "Unknown", type: "UNKNOWN_TYPE" },
          { id: "5", name: "G1", type: "GROUP" },
        ],
      };

      const stats = collectNodeStats(node);
      // 3 mergeable (2 VECTOR + 1 GROUP) out of 4
      expect(stats.mergeableRatio).toBe(0.75);
    });
  });
});

```

--------------------------------------------------------------------------------
/tests/unit/services/cache.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Cache Manager Unit Tests
 *
 * Tests the multi-layer caching system for Figma API responses and images.
 */

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { CacheManager } from "~/services/cache/index.js";

describe("CacheManager", () => {
  let cacheManager: CacheManager;
  let testCacheDir: string;

  beforeEach(() => {
    // Create a unique temporary directory for each test
    testCacheDir = path.join(os.tmpdir(), `figma-cache-test-${Date.now()}`);
    cacheManager = new CacheManager({
      enabled: true,
      memory: {
        maxNodeItems: 100,
        maxImageItems: 50,
        nodeTTL: 1000, // 1 second TTL for testing
        imageTTL: 1000,
      },
      disk: {
        cacheDir: testCacheDir,
        maxSize: 100 * 1024 * 1024,
        ttl: 1000, // 1 second TTL for testing
      },
    });
  });

  afterEach(() => {
    // Clean up test cache directory
    if (fs.existsSync(testCacheDir)) {
      fs.rmSync(testCacheDir, { recursive: true, force: true });
    }
  });

  describe("Configuration", () => {
    it("should create cache directories on initialization", () => {
      expect(fs.existsSync(testCacheDir)).toBe(true);
      expect(fs.existsSync(path.join(testCacheDir, "data"))).toBe(true);
      expect(fs.existsSync(path.join(testCacheDir, "images"))).toBe(true);
      expect(fs.existsSync(path.join(testCacheDir, "metadata"))).toBe(true);
    });

    it("should not create directories when disabled", () => {
      const disabledCacheDir = path.join(os.tmpdir(), `figma-cache-disabled-${Date.now()}`);
      new CacheManager({
        enabled: false,
        disk: {
          cacheDir: disabledCacheDir,
          maxSize: 100 * 1024 * 1024,
          ttl: 1000,
        },
      });

      expect(fs.existsSync(disabledCacheDir)).toBe(false);
    });

    it("should return correct cache stats", async () => {
      const stats = await cacheManager.getStats();

      expect(stats.enabled).toBe(true);
      expect(stats.memory.size).toBe(0);
      expect(stats.disk.nodeFileCount).toBe(0);
      expect(stats.disk.imageFileCount).toBe(0);
      expect(stats.disk.totalSize).toBe(0);
    });

    it("should return cache directory", () => {
      expect(cacheManager.getCacheDir()).toBe(testCacheDir);
    });

    it("should report enabled status", () => {
      expect(cacheManager.isEnabled()).toBe(true);
    });
  });

  describe("Node Data Caching", () => {
    const testData = { id: "123", name: "Test Node", type: "FRAME" };
    const fileKey = "test-file-key";

    it("should cache and retrieve node data", async () => {
      await cacheManager.setNodeData(testData, fileKey);
      const cached = await cacheManager.getNodeData(fileKey);

      expect(cached).toEqual(testData);
    });

    it("should cache with nodeId parameter", async () => {
      const nodeId = "node-456";
      await cacheManager.setNodeData(testData, fileKey, nodeId);
      const cached = await cacheManager.getNodeData(fileKey, nodeId);

      expect(cached).toEqual(testData);
    });

    it("should cache with depth parameter", async () => {
      const nodeId = "node-789";
      const depth = 3;
      await cacheManager.setNodeData(testData, fileKey, nodeId, depth);
      const cached = await cacheManager.getNodeData(fileKey, nodeId, depth);

      expect(cached).toEqual(testData);
    });

    it("should return null for non-existent cache", async () => {
      const cached = await cacheManager.getNodeData("non-existent-key");

      expect(cached).toBeNull();
    });

    it("should return null for expired cache", async () => {
      await cacheManager.setNodeData(testData, fileKey);

      // Wait for cache to expire (TTL is 1 second)
      await new Promise((resolve) => setTimeout(resolve, 1100));

      const cached = await cacheManager.getNodeData(fileKey);
      expect(cached).toBeNull();
    });

    it("should update cache stats after caching data", async () => {
      await cacheManager.setNodeData(testData, fileKey);
      const stats = await cacheManager.getStats();

      expect(stats.memory.size).toBe(1);
      expect(stats.disk.nodeFileCount).toBe(1);
      expect(stats.disk.totalSize).toBeGreaterThan(0);
    });

    it("should check if node data exists", async () => {
      expect(await cacheManager.hasNodeData(fileKey)).toBe(false);
      await cacheManager.setNodeData(testData, fileKey);
      expect(await cacheManager.hasNodeData(fileKey)).toBe(true);
    });
  });

  describe("Image Caching", () => {
    const fileKey = "test-file";
    const nodeId = "image-node";
    const format = "png";
    let testImagePath: string;

    beforeEach(() => {
      // Create a test image file
      testImagePath = path.join(os.tmpdir(), `test-image-${Date.now()}.png`);
      fs.writeFileSync(testImagePath, Buffer.from([0x89, 0x50, 0x4e, 0x47])); // PNG header
    });

    afterEach(() => {
      if (fs.existsSync(testImagePath)) {
        fs.unlinkSync(testImagePath);
      }
    });

    it("should return null for uncached image", async () => {
      const result = await cacheManager.hasImage(fileKey, nodeId, format);
      expect(result).toBeNull();
    });

    it("should cache and find image", async () => {
      await cacheManager.cacheImage(testImagePath, fileKey, nodeId, format);
      const cachedPath = await cacheManager.hasImage(fileKey, nodeId, format);

      expect(cachedPath).not.toBeNull();
      expect(fs.existsSync(cachedPath!)).toBe(true);
    });

    it("should copy image from cache to target path", async () => {
      await cacheManager.cacheImage(testImagePath, fileKey, nodeId, format);

      const targetPath = path.join(os.tmpdir(), `target-${Date.now()}.png`);
      const success = await cacheManager.copyImageFromCache(fileKey, nodeId, format, targetPath);

      expect(success).toBe(true);
      expect(fs.existsSync(targetPath)).toBe(true);

      // Clean up
      fs.unlinkSync(targetPath);
    });

    it("should return false when copying non-existent image", async () => {
      const targetPath = path.join(os.tmpdir(), `target-${Date.now()}.png`);
      const success = await cacheManager.copyImageFromCache(
        "non-existent",
        "non-existent",
        format,
        targetPath,
      );

      expect(success).toBe(false);
    });

    it("should update cache stats after caching image", async () => {
      await cacheManager.cacheImage(testImagePath, fileKey, nodeId, format);
      const stats = await cacheManager.getStats();

      expect(stats.disk.imageFileCount).toBe(1);
    });
  });

  describe("Cache Cleanup", () => {
    it("should clean expired cache entries", async () => {
      const testData = { id: "test" };
      await cacheManager.setNodeData(testData, "file-1");

      // Wait for cache to expire
      await new Promise((resolve) => setTimeout(resolve, 1100));

      const result = await cacheManager.cleanExpired();
      expect(result.disk).toBeGreaterThanOrEqual(1);

      const stats = await cacheManager.getStats();
      expect(stats.disk.nodeFileCount).toBe(0);
    });

    it("should clear all cache", async () => {
      await cacheManager.setNodeData({ id: "1" }, "file-1");
      await cacheManager.setNodeData({ id: "2" }, "file-2");

      await cacheManager.clearAll();

      const stats = await cacheManager.getStats();
      expect(stats.memory.size).toBe(0);
      expect(stats.disk.nodeFileCount).toBe(0);
      expect(stats.disk.imageFileCount).toBe(0);
    });
  });

  describe("Cache Invalidation", () => {
    it("should invalidate all entries for a file", async () => {
      const fileKey = "test-file";
      await cacheManager.setNodeData({ id: "1" }, fileKey, "node-1");
      await cacheManager.setNodeData({ id: "2" }, fileKey, "node-2");
      await cacheManager.setNodeData({ id: "3" }, "other-file", "node-3");

      const result = await cacheManager.invalidateFile(fileKey);

      expect(result.memory).toBe(2);
      expect(await cacheManager.getNodeData(fileKey, "node-1")).toBeNull();
      expect(await cacheManager.getNodeData(fileKey, "node-2")).toBeNull();
      // Other file should still be cached
      expect(await cacheManager.getNodeData("other-file", "node-3")).not.toBeNull();
    });

    it("should invalidate a specific node", async () => {
      const fileKey = "test-file";
      await cacheManager.setNodeData({ id: "1" }, fileKey, "node-1");
      await cacheManager.setNodeData({ id: "2" }, fileKey, "node-2");

      const result = await cacheManager.invalidateNode(fileKey, "node-1");

      expect(result.memory).toBe(1);
      expect(await cacheManager.getNodeData(fileKey, "node-1")).toBeNull();
      expect(await cacheManager.getNodeData(fileKey, "node-2")).not.toBeNull();
    });
  });

  describe("Disabled Cache", () => {
    let disabledCacheManager: CacheManager;

    beforeEach(() => {
      disabledCacheManager = new CacheManager({ enabled: false });
    });

    it("should return null for getNodeData when disabled", async () => {
      const result = await disabledCacheManager.getNodeData("any-key");
      expect(result).toBeNull();
    });

    it("should do nothing for setNodeData when disabled", async () => {
      await disabledCacheManager.setNodeData({ id: "test" }, "file-key");
      const result = await disabledCacheManager.getNodeData("file-key");
      expect(result).toBeNull();
    });

    it("should return null for hasImage when disabled", async () => {
      const result = await disabledCacheManager.hasImage("file", "node", "png");
      expect(result).toBeNull();
    });

    it("should return source path for cacheImage when disabled", async () => {
      const sourcePath = "/path/to/image.png";
      const result = await disabledCacheManager.cacheImage(sourcePath, "file", "node", "png");
      expect(result).toBe(sourcePath);
    });

    it("should return zero for cleanExpired when disabled", async () => {
      const result = await disabledCacheManager.cleanExpired();
      expect(result.memory).toBe(0);
      expect(result.disk).toBe(0);
    });

    it("should report disabled in stats", async () => {
      const stats = await disabledCacheManager.getStats();
      expect(stats.enabled).toBe(false);
    });

    it("should report disabled status", () => {
      expect(disabledCacheManager.isEnabled()).toBe(false);
    });
  });

  describe("Memory Cache (L1)", () => {
    it("should serve from memory cache on second read", async () => {
      const testData = { id: "memory-test" };
      const fileKey = "memory-file";

      await cacheManager.setNodeData(testData, fileKey);

      // First read - populates memory from disk if needed
      const first = await cacheManager.getNodeData(fileKey);
      expect(first).toEqual(testData);

      // Second read should hit memory cache
      const second = await cacheManager.getNodeData(fileKey);
      expect(second).toEqual(testData);

      const stats = await cacheManager.getStats();
      expect(stats.memory.hits).toBeGreaterThan(0);
    });

    it("should track cache statistics", async () => {
      // Initial stats
      let stats = await cacheManager.getStats();
      expect(stats.memory.hits).toBe(0);
      expect(stats.memory.misses).toBe(0);

      // Miss
      await cacheManager.getNodeData("non-existent");
      stats = await cacheManager.getStats();
      expect(stats.memory.misses).toBe(1);

      // Set and hit
      await cacheManager.setNodeData({ test: 1 }, "key");
      await cacheManager.getNodeData("key");
      stats = await cacheManager.getStats();
      expect(stats.memory.hits).toBe(1);
    });

    it("should reset statistics", async () => {
      await cacheManager.setNodeData({ test: 1 }, "key");
      await cacheManager.getNodeData("key");
      await cacheManager.getNodeData("non-existent");

      cacheManager.resetStats();
      const stats = await cacheManager.getStats();
      expect(stats.memory.hits).toBe(0);
      expect(stats.memory.misses).toBe(0);
    });
  });
});

```

--------------------------------------------------------------------------------
/tests/integration/output-quality.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Output Quality Validation Tests
 *
 * Tests the quality of optimized output for redundancy, consistency, and correctness.
 * Converted from scripts/analyze-optimized-output.ts
 */

import { describe, it, expect, beforeAll } from "vitest";
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import { parseFigmaResponse } from "~/core/parser.js";
import type { SimplifiedNode } from "~/types/index.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fixturesDir = path.join(__dirname, "../fixtures/figma-data");

// Test file configurations
const TEST_FILES = [
  { name: "node-402-34955", desc: "Group 1410104853 (1580x895)" },
  { name: "node-240-32163", desc: "Call Logs 有数据 (375x827)" },
];

// Quality analysis interfaces
interface QualityAnalysis {
  totalNodes: number;
  nodesByType: Record<string, number>;
  layoutStats: {
    flex: number;
    grid: number;
    absolute: number;
    none: number;
  };
  cssPropertyUsage: Record<string, number>;
  redundantPatterns: RedundantPattern[];
  emptyOrDefaultValues: EmptyValue[];
  issues: QualityIssue[];
}

interface RedundantPattern {
  nodeName: string;
  pattern: string;
  details: string;
}

interface EmptyValue {
  nodeName: string;
  property: string;
  value: string;
}

interface QualityIssue {
  nodeName: string;
  nodeType: string;
  issue: string;
  severity: "warning" | "error";
}

// Helper: Analyze a node recursively
function analyzeNode(node: SimplifiedNode, result: QualityAnalysis, parentLayout?: string): void {
  result.totalNodes++;

  // Count node types
  result.nodesByType[node.type] = (result.nodesByType[node.type] || 0) + 1;

  // Count layout types
  const display = node.cssStyles?.display;
  if (display === "flex") {
    result.layoutStats.flex++;
  } else if (display === "grid") {
    result.layoutStats.grid++;
  } else if (node.cssStyles?.position === "absolute") {
    result.layoutStats.absolute++;
  } else {
    result.layoutStats.none++;
  }

  // Analyze CSS properties
  if (node.cssStyles) {
    for (const [key, value] of Object.entries(node.cssStyles)) {
      if (value !== undefined && value !== null && value !== "") {
        result.cssPropertyUsage[key] = (result.cssPropertyUsage[key] || 0) + 1;
      }

      // Check for empty or default values
      if (value === "" || value === "0" || value === "0px" || value === "none") {
        result.emptyOrDefaultValues.push({
          nodeName: node.name,
          property: key,
          value: String(value),
        });
      }

      // Check for redundant patterns
      // 1. position: absolute inside flex/grid parent
      if (key === "position" && value === "absolute" && parentLayout) {
        if (parentLayout === "flex" || parentLayout === "grid") {
          result.redundantPatterns.push({
            nodeName: node.name,
            pattern: "absolute-in-layout",
            details: `position:absolute inside ${parentLayout} parent`,
          });
        }
      }

      // 2. width with flex property (potential conflict)
      if (key === "width" && node.cssStyles?.flex) {
        result.redundantPatterns.push({
          nodeName: node.name,
          pattern: "width-with-flex",
          details: "width specified with flex property",
        });
      }
    }
  }

  // Check for quality issues
  // 1. TEXT node with layout properties
  if (node.type === "TEXT" && (display === "flex" || display === "grid")) {
    result.issues.push({
      nodeName: node.name,
      nodeType: node.type,
      issue: `TEXT node with ${display} layout (unnecessary)`,
      severity: "warning",
    });
  }

  // 2. Empty children array
  if (node.children && node.children.length === 0) {
    result.issues.push({
      nodeName: node.name,
      nodeType: node.type,
      issue: "Empty children array (should be removed)",
      severity: "warning",
    });
  }

  // 3. VECTOR/ELLIPSE without exportInfo
  if ((node.type === "VECTOR" || node.type === "ELLIPSE") && !node.exportInfo) {
    result.issues.push({
      nodeName: node.name,
      nodeType: node.type,
      issue: `${node.type} without exportInfo (image not exported)`,
      severity: "warning",
    });
  }

  // Recurse into children
  if (node.children) {
    const currentLayout =
      display || (node.cssStyles?.position === "absolute" ? "absolute" : undefined);
    for (const child of node.children) {
      analyzeNode(child, result, currentLayout);
    }
  }
}

// Helper: Analyze a parsed result
function analyzeOutput(result: ReturnType<typeof parseFigmaResponse>): QualityAnalysis {
  const analysis: QualityAnalysis = {
    totalNodes: 0,
    nodesByType: {},
    layoutStats: { flex: 0, grid: 0, absolute: 0, none: 0 },
    cssPropertyUsage: {},
    redundantPatterns: [],
    emptyOrDefaultValues: [],
    issues: [],
  };

  for (const node of result.nodes) {
    analyzeNode(node, analysis);
  }

  return analysis;
}

// Helper: Load and parse fixture
function loadAndParse(name: string): ReturnType<typeof parseFigmaResponse> {
  const filePath = path.join(fixturesDir, `${name}.json`);
  const rawData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
  return parseFigmaResponse(rawData);
}

describe("Output Quality Validation", () => {
  TEST_FILES.forEach(({ name, desc }) => {
    describe(`${name} (${desc})`, () => {
      let result: ReturnType<typeof parseFigmaResponse>;
      let analysis: QualityAnalysis;

      beforeAll(() => {
        const filePath = path.join(fixturesDir, `${name}.json`);
        if (!fs.existsSync(filePath)) {
          throw new Error(`Test fixture not found: ${name}.json`);
        }

        result = loadAndParse(name);
        analysis = analyzeOutput(result);
      });

      describe("Node Structure", () => {
        it("should have non-zero node count", () => {
          expect(analysis.totalNodes).toBeGreaterThan(0);
        });

        it("should have diverse node types", () => {
          const typeCount = Object.keys(analysis.nodesByType).length;
          expect(typeCount).toBeGreaterThan(1);
        });

        it("should have reasonable node type distribution", () => {
          // No single type should dominate excessively (>90%)
          const maxTypeCount = Math.max(...Object.values(analysis.nodesByType));
          const dominanceRatio = maxTypeCount / analysis.totalNodes;
          expect(dominanceRatio).toBeLessThan(0.9);
        });
      });

      describe("Layout Quality", () => {
        it("should use semantic layouts (flex/grid)", () => {
          const semanticLayouts = analysis.layoutStats.flex + analysis.layoutStats.grid;
          expect(semanticLayouts).toBeGreaterThan(0);
        });

        it("should have reasonable absolute positioning ratio", () => {
          const absoluteRatio = analysis.layoutStats.absolute / analysis.totalNodes;
          // Warning if >80% absolute (but not a hard failure for all fixtures)
          expect(absoluteRatio).toBeLessThan(0.95);
        });
      });

      describe("CSS Property Quality", () => {
        it("should have essential CSS properties", () => {
          // Width and height should be commonly used
          const hasWidth = (analysis.cssPropertyUsage["width"] || 0) > 0;
          const hasHeight = (analysis.cssPropertyUsage["height"] || 0) > 0;
          expect(hasWidth || hasHeight).toBe(true);
        });

        it("should not have excessive empty or default values", () => {
          // Empty values should be less than 50% of total nodes
          // Note: Some default values like "0px" may be intentional for clarity
          const emptyRatio = analysis.emptyOrDefaultValues.length / analysis.totalNodes;
          expect(emptyRatio).toBeLessThan(0.5);
        });

        it("should have consistent property usage", () => {
          // If display is used, it should be meaningful
          const displayCount = analysis.cssPropertyUsage["display"] || 0;
          if (displayCount > 0) {
            // Display should be on containers, not every node
            expect(displayCount).toBeLessThan(analysis.totalNodes);
          }
        });
      });

      describe("Redundancy Check", () => {
        it("should minimize position:absolute inside flex/grid children", () => {
          const absoluteInLayout = analysis.redundantPatterns.filter(
            (p) => p.pattern === "absolute-in-layout",
          );
          // Allow some absolute positioning for:
          // - Overlapping elements that need stacking
          // - Non-homogeneous elements in grid containers (e.g., tabs, dividers)
          // These are intentionally kept absolute to preserve their original position
          const ratio = absoluteInLayout.length / analysis.totalNodes;
          expect(ratio).toBeLessThan(0.1); // Allow up to 10%
        });

        it("should not have conflicting width and flex properties", () => {
          const widthWithFlex = analysis.redundantPatterns.filter(
            (p) => p.pattern === "width-with-flex",
          );
          // Warning level - not necessarily wrong but worth noting
          // Allow up to 5% of nodes to have this pattern
          const ratio = widthWithFlex.length / analysis.totalNodes;
          expect(ratio).toBeLessThan(0.05);
        });
      });

      describe("Quality Issues", () => {
        it("should not have TEXT nodes with layout properties", () => {
          const textWithLayout = analysis.issues.filter(
            (i) => i.nodeType === "TEXT" && i.issue.includes("layout"),
          );
          expect(textWithLayout.length).toBe(0);
        });

        it("should not have empty children arrays", () => {
          const emptyChildren = analysis.issues.filter((i) => i.issue.includes("Empty children"));
          expect(emptyChildren.length).toBe(0);
        });

        it("should have exportInfo for vector graphics", () => {
          const vectorsWithoutExport = analysis.issues.filter((i) =>
            i.issue.includes("without exportInfo"),
          );
          // Allow some vectors without export (decorative elements)
          const vectorCount =
            (analysis.nodesByType["VECTOR"] || 0) + (analysis.nodesByType["ELLIPSE"] || 0);
          if (vectorCount > 0) {
            const missingExportRatio = vectorsWithoutExport.length / vectorCount;
            expect(missingExportRatio).toBeLessThan(0.5);
          }
        });
      });

      describe("Output Statistics", () => {
        it("should produce consistent layout statistics", () => {
          // Snapshot the statistics for regression detection
          expect({
            totalNodes: analysis.totalNodes,
            flexCount: analysis.layoutStats.flex,
            gridCount: analysis.layoutStats.grid,
            absoluteCount: analysis.layoutStats.absolute,
            issueCount: analysis.issues.length,
            redundantCount: analysis.redundantPatterns.length,
          }).toMatchSnapshot();
        });
      });
    });
  });

  describe("Cross-fixture Consistency", () => {
    let analyses: Map<string, QualityAnalysis>;

    beforeAll(() => {
      analyses = new Map();
      TEST_FILES.forEach(({ name }) => {
        const filePath = path.join(fixturesDir, `${name}.json`);
        if (fs.existsSync(filePath)) {
          const result = loadAndParse(name);
          analyses.set(name, analyzeOutput(result));
        }
      });
    });

    it("should use consistent CSS properties across fixtures", () => {
      const allProperties = new Set<string>();
      analyses.forEach((analysis) => {
        Object.keys(analysis.cssPropertyUsage).forEach((prop) => {
          allProperties.add(prop);
        });
      });

      // Essential properties should appear in all fixtures
      const essentialProps = ["width", "height"];
      essentialProps.forEach((prop) => {
        let count = 0;
        analyses.forEach((analysis) => {
          if (analysis.cssPropertyUsage[prop]) count++;
        });
        expect(count).toBeGreaterThan(0);
      });
    });

    it("should have similar quality metrics across fixtures", () => {
      const qualityScores: number[] = [];

      analyses.forEach((analysis) => {
        // Quality score: higher is better
        const semanticRatio =
          (analysis.layoutStats.flex + analysis.layoutStats.grid) / analysis.totalNodes;
        const issueRatio = analysis.issues.length / analysis.totalNodes;
        const score = semanticRatio * 100 - issueRatio * 50;
        qualityScores.push(score);
      });

      // All fixtures should have non-negative quality scores
      qualityScores.forEach((score) => {
        expect(score).toBeGreaterThanOrEqual(-10);
      });
    });
  });
});

```
Page 1/6FirstPrevNextLast