diff --git a/.agents/skills/backend-development/SKILL.md b/.agents/skills/backend-development/SKILL.md new file mode 100644 index 0000000..df6a99f --- /dev/null +++ b/.agents/skills/backend-development/SKILL.md @@ -0,0 +1,283 @@ +--- +name: backend-development +description: | + MiniHES 后端开发规范。当用户进行 FastAPI 后端开发、API 设计、数据库操作、 + DLMS 协议实现、数据采集任务开发时自动激活。 + 涵盖分层架构、编码原则、安全要求、数据库规范。 +--- + +# MiniHES 后端开发规范 + +## 何时使用 + +- 编写 FastAPI endpoint、service、model、schema +- 数据库迁移、种子数据 +- DLMS 协议栈开发 +- 数据采集任务调度 +- 后端代码审查或重构 + +## 技术栈 + +- **框架**: FastAPI + uvicorn +- **语言**: Python 3.12 (uv 管理) +- **ORM**: SQLAlchemy 2.0 async (asyncpg) +- **数据库**: PostgreSQL + Redis + InfluxDB +- **迁移**: Alembic +- **任务调度**: APScheduler +- **验证**: Pydantic v2 + +## 分层架构 + +严格遵循单向依赖,禁止跨层调用: + +``` +api/v1/endpoints/ → 路由层(HTTP 处理) + ↓ +schemas/ → 数据验证层(Pydantic) + ↓ +services/ → 业务逻辑层 + ↓ +models/ → 数据访问层(SQLAlchemy ORM) + ↓ +core/ → 基础设施(config, database, security) +``` + +### 各层职责 + +| 层 | 职责 | 禁止 | +|---|------|------| +| endpoints | 参数接收、调用 service、返回响应 | 直接操作 ORM、写业务逻辑 | +| schemas | 请求/响应模型、数据验证 | 访问数据库 | +| services | 业务逻辑、事务管理、跨模型协调 | 直接处理 HTTP 请求 | +| models | ORM 定义、表结构 | 包含业务逻辑 | +| core | 配置、数据库连接、安全工具 | 依赖业务层 | + +## 编码原则 + +### 先设计后编码 + +1. 先确认需求和接口设计 +2. 定义 schema(请求/响应模型) +3. 实现 service 层业务逻辑 +4. 实现 endpoint 路由 +5. 编写测试 + +**禁止**:未确认方案就直接改数据库或写代码。 + +### 命名规范 + +| 类型 | 规范 | 示例 | +|------|------|------| +| 表名 | `{模块前缀}_{名称}` | `dev_meter`, `col_task` | +| 模型类 | PascalCase | `MeterComm`, `TaskLog` | +| 字段名 | snake_case | `meter_id`, `created_at` | +| API 路径 | kebab-case 复数 | `/api/v1/meter-points` | +| Service 方法 | 动词_名词 | `get_meter`, `create_task` | +| Schema 类 | `{动作}{资源}{Request/Response}` | `CreateMeterRequest` | + +### 表前缀约定 + +| 前缀 | 模块 | 示例 | +|------|------|------| +| `sys_` | 系统管理 | `sys_user`, `sys_role`, `sys_audit_log` | +| `dev_` | 设备管理 | `dev_meter`, `dev_meter_comm` | +| `col_` | 数据采集 | `col_task`, `col_meter_reading` | +| `lab_` | 实验室测试 | `lab_test_task`, `lab_defect` | + +## 响应格式 + +### 成功响应 + +直接返回数据,使用标准 HTTP 状态码: + +```python +# 200 OK - 查询 +@router.get("/meters") +async def list_meters(...): + return {"items": [...], "total": 100} + +# 201 Created - 创建 +@router.post("/meters", status_code=201) +async def create_meter(...): + return meter +``` + +### 错误响应 + +HTTP 状态码 + 业务码: + +```python +{ + "detail": "错误描述", + "error_code": 10001 +} +``` + +### 业务错误码 + +| 范围 | 模块 | 示例 | +|------|------|------| +| 10001-10999 | 设备管理 | 10001=设备不存在 | +| 20001-20999 | 数据采集 | 20001=任务执行失败 | +| 30001-30999 | 系统管理 | 30001=认证失败 | + +## 数据库规范 + +### ORM 模型 + +```python +from app.core.database import Base, TimestampMixin + +class Meter(Base, TimestampMixin): + __tablename__ = "dev_meter" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + serial_number: Mapped[str] = mapped_column(String(50), unique=True, index=True) + # ... 其他字段 + + # relationship 使用字符串引用避免循环导入 + comm: Mapped["MeterComm | None"] = relationship(back_populates="meter", uselist=False) +``` + +### 关键规则 + +- 所有模型继承 `Base`,需要时间戳的加 `TimestampMixin` +- `Mapped` 类型注解,必须指定 `nullable` +- JSON 字段用 `Mapped[dict]` + `mapped_column(JSON, ...)` +- ForeignKey 必须显式声明,有 `ondelete` 约束 +- 新模型必须在 `app/models/__init__.py` 注册 + +### 迁移流程 + +```bash +# 1. 生成迁移(先确认模型变更正确) +uv run alembic revision --autogenerate -m "描述" + +# 2. 检查迁移文件(特别是 nullable、default、现有数据) + +# 3. 应用迁移 +uv run alembic upgrade head +``` + +**迁移注意**: +- 有现有数据的表添加 NOT NULL 列时,需先填充默认值或先加 nullable 列再改 +- 删除列前确认无代码引用 +- 不可回滚的迁移需确认 + +## 安全要求 + +### 必须 + +- 所有用户输入通过 Pydantic schema 验证 +- 使用参数化查询(SQLAlchemy ORM 自动处理) +- JWT token 认证保护需要登录的接口 +- 密码使用 bcrypt/pbkdf2 哈希存储 +- API 路径中不暴露内部 ID 结构 + +### 禁止 + +- 拼接 SQL 字符串 +- 硬编码密钥或密码 +- 返回完整异常堆栈给前端 +- 未验证的外部输入直接传入数据库查询 + +## 输出格式 + +构建后端功能时,按以下格式输出: + +1. **文件结构** — 展示代码应放在哪里 +2. **完整代码** — 功能完整、有类型注解的代码 +3. **依赖** — 需要安装的包(`uv add xxx`) +4. **数据库迁移** — 如有模型变更,提供 alembic 命令 +5. **环境变量** — 如有需要,提供 `.env` 配置 +6. **运行说明** — 如何启动和验证 + +### 完整功能示例 + +**Schema 层**: +```python +# schemas/meter.py +from pydantic import BaseModel, Field + +class CreateMeterRequest(BaseModel): + serial_number: str = Field(..., max_length=50) + meter_name: str = Field(..., max_length=100) + meter_type_id: int | None = None + +class MeterDetail(BaseModel): + id: int + serial_number: str + meter_name: str + current_status: str + + model_config = {"from_attributes": True} +``` + +**Service 层**: +```python +# services/meter_service.py +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from app.models import Meter +from app.schemas.meter import CreateMeterRequest + +async def create_meter(db: AsyncSession, data: CreateMeterRequest) -> Meter: + meter = Meter(**data.model_dump()) + db.add(meter) + await db.flush() + return meter +``` + +**Endpoint 层**: +```python +# api/v1/endpoints/meters.py +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession +from app.core.dependencies import get_db, get_current_user +from app.schemas.meter import CreateMeterRequest, MeterDetail +from app.services.meter_service import create_meter + +router = APIRouter(prefix="/meters", tags=["meters"]) + +@router.post("", response_model=MeterDetail, status_code=201) +async def create( + data: CreateMeterRequest, + db: AsyncSession = Depends(get_db), + user=Depends(get_current_user), +): + return await create_meter(db, data) +``` + +**依赖安装**: +```bash +uv add # 如果需要新包 +uv run alembic revision --autogenerate -m "add xxx" +uv run alembic upgrade head +``` + +## 文件结构参考 + +``` +app/ +├── api/v1/endpoints/ # 每个业务一个文件 +│ ├── auth.py +│ ├── meters.py +│ ├── tasks.py +│ └── ... +├── schemas/ # 对应 endpoint 的请求/响应模型 +│ ├── auth.py +│ ├── meter.py +│ └── ... +├── services/ # 业务逻辑,按模块组织 +│ ├── meter_service.py +│ ├── task_service.py +│ └── ... +├── models/ # ORM 模型 +├── core/ # 基础设施 +│ ├── config.py # Settings +│ ├── database.py # engine, session, Base +│ ├── security.py # JWT, password +│ ├── dependencies.py # get_db, get_current_user +│ └── exceptions.py # 自定义异常 +└── main.py +``` diff --git "a/.agents/skills/checklists/\351\253\230\351\242\221\351\227\256\351\242\230\346\243\200\346\237\245\346\270\205\345\215\225.md" "b/.agents/skills/checklists/\351\253\230\351\242\221\351\227\256\351\242\230\346\243\200\346\237\245\346\270\205\345\215\225.md" new file mode 100644 index 0000000..24790c1 --- /dev/null +++ "b/.agents/skills/checklists/\351\253\230\351\242\221\351\227\256\351\242\230\346\243\200\346\237\245\346\270\205\345\215\225.md" @@ -0,0 +1,41 @@ +# 高频问题检查清单 + +技术骨干在疑难排查「阶段1:高频问题快速检查」中必须首先查阅本清单;发现新高频问题时应更新项目内副本 `.vibe/docs/常见问题检查清单.md`。 + +--- + +## API路径问题检查清单 + +- [ ] 路径是否缺少斜杠(/) +- [ ] 路径大小写是否正确 +- [ ] 相对路径 vs 绝对路径混淆 +- [ ] 代理配置是否正确 +- [ ] CORS 设置问题 + +## 环境变量问题检查清单 + +- [ ] 变量是否在正确环境加载 +- [ ] 变量名拼写错误 +- [ ] 变量值格式错误(引号、特殊字符) +- [ ] 变量作用域问题(全局 vs 局部) +- [ ] 敏感信息是否正确注入 + +## 端口问题检查清单 + +- [ ] 端口是否被占用 (`lsof -i :port`) +- [ ] 端口配置前后端是否一致 +- [ ] 防火墙设置 +- [ ] Docker 容器端口映射 +- [ ] HTTPS vs HTTP 协议混淆 + +## 启动脚本问题检查清单 + +- [ ] 脚本执行权限 +- [ ] 工作目录是否正确 +- [ ] 依赖是否安装完整 +- [ ] 配置文件路径是否正确 +- [ ] 日志级别设置 + +--- + +**使用说明**:排查前先按本清单逐项排除;发现新高频模式时,将条目补充到 `.vibe/docs/常见问题检查清单.md`,并可在本文件中同步增加分类(可选)。 diff --git a/.agents/skills/code-review/SKILL.md b/.agents/skills/code-review/SKILL.md new file mode 100644 index 0000000..1e5c5da --- /dev/null +++ b/.agents/skills/code-review/SKILL.md @@ -0,0 +1,171 @@ +--- +name: code-review +description: | + MiniHES 代码审查。当用户请求审查代码、review PR、检查变更时激活。 + 支持本地变更审查和指定文件审查,输出结构化审查报告。 + 引用 backend-checklist.md 和 frontend-checklist.md 作为审查依据。 +--- + +# MiniHES 代码审查 + +## 何时使用 + +- 用户说 "review"、"审查"、"检查代码" +- 提交 PR 前的代码审查 +- 审查指定文件或目录 +- 审查 git diff 中的变更 + +## 审查模式 + +### 模式 A:本地变更审查 + +```bash +# 查看变更范围 +git status +git diff # 未暂存 +git diff --staged # 已暂存 +``` + +审查所有待提交变更。 + +### 模式 B:指定文件审查 + +用户指定文件路径,逐文件按清单审查。 + +### 模式 C:PR 审查 + +```bash +# 获取 PR 变更 +git diff main...HEAD --stat +git diff main...HEAD +``` + +## 审查维度 + +按以下六大支柱分析代码: + +### 1. 正确性 +- 逻辑是否正确,是否实现预期功能 +- 边界条件是否处理 +- 异步操作是否正确 await +- 数据库事务是否合理 + +### 2. 安全性 +- 输入是否验证 +- SQL 注入风险 +- XSS / CSRF 防护 +- 敏感信息是否泄露 +- 权限检查是否存在 + +### 3. 性能 +- N+1 查询问题 +- 不必要的全表扫描 +- 内存泄漏风险 +- 大量数据的分页处理 + +### 4. 可维护性 +- 代码是否清晰易懂 +- 命名是否准确 +- 函数是否过长(< 50 行) +- 是否有重复代码可提取 + +### 5. 架构合规 +- 是否遵循分层架构(endpoints → schemas → services → models) +- 是否跨层调用 +- 是否符合项目命名规范 +- 是否符合表前缀约定 + +### 6. 测试覆盖 +- 关键逻辑是否有测试 +- 边界条件是否覆盖 +- 错误路径是否测试 + +## 审查清单 + +- 后端代码:参见 [backend-checklist.md](references/backend-checklist.md) +- 前端代码:参见 [frontend-checklist.md](references/frontend-checklist.md) + +## 输出模板 + +### Template A:有发现 + +```markdown +# Code Review + +Found N urgent issues: + +## 1 +FilePath: line + + +### Suggested fix + + +--- + +Found M suggestions for improvement: + +## 1 +FilePath: line + + +### Suggested fix + + +--- +``` + +问题超过 10 条时,输出前 10 条并注明 "10+ issues"。 + +如果存在需要修改代码的问题,末尾询问:**"是否需要我应用这些修复建议?"** + +### Template B:无问题 + +```markdown +# Code Review +No issues found. +``` + +## 审查决策树 + +每次审查前,按此决策树判断审查重点: + +``` +代码变更 → 是否涉及数据库? +├─ 是 → 检查迁移、ForeignKey、索引、N+1 +│ +├─ 否 → 是否涉及 API 端点? +│ ├─ 是 → 检查 schema 验证、状态码、权限 +│ │ +│ └─ 否 → 是否涉及前端组件? +│ ├─ 是 → 检查类型安全、状态管理、交互反馈 +│ │ +│ └─ 否 → 通用审查(命名、结构、安全) +``` + +## 常见问题 → 修复方案 + +| 错误模式 | 检测方式 | 修复方案 | +|---------|---------|---------| +| `Mapped[dict]` 无 JSON 类型 | ruff 检查 model 文件 | 添加 `mapped_column(JSON, ...)` | +| async 函数内用 `time.sleep` | grep `time.sleep` | 替换为 `asyncio.sleep` | +| 未注册的新模型 | 检查 `models/__init__.py` | 添加导入和 `__all__` | +| NOT NULL 列无默认值且有数据 | alembic migrate 报错 | 先加 nullable 列 → 填充数据 → 改 NOT NULL | +| 前端直接用 axios | grep `axios` 或 `fetch` | 统一使用 `requestClient` | +| 组件内硬编码 API 地址 | grep `http://` / `https://` | 使用环境变量 | +| 缺少 loading 状态 | 检查异步操作 | 添加 `loading ref` + `a-spin` | +| 缺少删除确认 | 检查删除按钮 | 添加 `Modal.confirm()` | + +## 审查流程 + +1. **识别变更范围**:确定审查哪些文件 +2. **分类**:区分后端 (.py) 和前端 (.vue/.ts) 文件 +3. **逐文件审查**:按对应清单检查 +4. **汇总**:按严重程度(Critical → Suggestion)排列 +5. **输出**:使用 Template A 或 B + +## 审查语气 + +- 建设性、专业、简洁 +- 说明**为什么**需要修改 +- 认可好的代码实践 diff --git a/.agents/skills/code-review/references/backend-checklist.md b/.agents/skills/code-review/references/backend-checklist.md new file mode 100644 index 0000000..8ca3bd8 --- /dev/null +++ b/.agents/skills/code-review/references/backend-checklist.md @@ -0,0 +1,57 @@ +# 后端代码审查清单 + +## 架构合规 + +- [ ] endpoint 只处理 HTTP 逻辑,不含业务代码 +- [ ] service 层处理业务逻辑,不直接操作 request/response +- [ ] schema 定义了请求/响应类型 +- [ ] 数据库操作通过 ORM,无原生 SQL 拼接 +- [ ] 新模型在 `__init__.py` 注册 + +## 数据库 + +- [ ] 新表有正确的模块前缀 (sys_/dev_/col_/lab_) +- [ ] ForeignKey 声明正确,有 ondelete 约束 +- [ ] 查询有合理的索引支持 +- [ ] 无 N+1 查询(使用 `selectinload` / `joinedload`) +- [ ] 批量操作使用批量插入而非循环单条 +- [ ] 大量数据查询有分页限制 + +## 安全 + +- [ ] 用户输入通过 Pydantic schema 验证 +- [ ] 无 SQL 字符串拼接 +- [ ] 需要认证的接口有依赖注入检查 +- [ ] 敏感操作有权限校验 +- [ ] 密码/密钥不硬编码、不输出到日志 +- [ ] 错误响应不暴露堆栈信息 + +## API 设计 + +- [ ] 使用正确的 HTTP 方法 (GET/POST/PUT/PATCH/DELETE) +- [ ] 使用正确的 HTTP 状态码 +- [ ] 响应格式符合项目规范 +- [ ] API 路径使用 kebab-case 复数形式 +- [ ] 分页接口有 `page`、`page_size`、`total` 字段 + +## 异步 + +- [ ] async 函数正确使用 await +- [ ] 数据库操作使用 async session +- [ ] 无阻塞调用在 async 上下文中(如 time.sleep → asyncio.sleep) +- [ ] 并发操作使用 asyncio.gather + +## 错误处理 + +- [ ] 可预期的错误有明确的异常处理 +- [ ] 异常有合理的错误码和描述 +- [ ] 数据库事务失败时正确 rollback +- [ ] 外部调用(网络、Redis)有超时和重试 + +## 代码质量 + +- [ ] 函数长度 < 50 行 +- [ ] 无重复代码 +- [ ] 变量命名准确 +- [ ] 无未使用的导入 +- [ ] 类型注解完整 diff --git a/.agents/skills/code-review/references/frontend-checklist.md b/.agents/skills/code-review/references/frontend-checklist.md new file mode 100644 index 0000000..741f757 --- /dev/null +++ b/.agents/skills/code-review/references/frontend-checklist.md @@ -0,0 +1,62 @@ +# 前端代码审查清单 + +## 组件设计 + +- [ ] 组件职责单一 +- [ ] Props 有 TypeScript 类型定义 +- [ ] Events 使用 emit,不直接修改 props +- [ ] 复杂逻辑提取为 composable +- [ ] 列表渲染使用 `:key` +- [ ] 无直接 DOM 操作 + +## 类型安全 + +- [ ] API 响应有类型定义 +- [ ] Props 有类型定义和默认值 +- [ ] ref/reactive 有泛型类型 +- [ ] 无 any 类型(除非必要且注释说明) +- [ ] 事件回调有类型 + +## 状态管理 + +- [ ] 组件内状态用 ref/reactive +- [ ] 跨组件共享用 Pinia Store +- [ ] Store 内异步操作有 loading 状态 +- [ ] Store 无冗余状态(可从其他状态派生) + +## 性能 + +- [ ] 路由使用懒加载 +- [ ] 大列表考虑虚拟滚动 +- [ ] 避免不必要的重渲染(computed vs watch) +- [ ] 图片有懒加载 +- [ ] 无内存泄漏(定时器、事件监听器在 unmount 时清理) + +## 样式 + +- [ ] 使用 scoped 样式 +- [ ] 无全局样式污染 +- [ ] 使用 Ant Design Token/变量,非硬编码颜色 +- [ ] 响应式适配(至少 1280px+) + +## API 调用 + +- [ ] 使用 requestClient 封装 +- [ ] 请求/响应有 TypeScript 类型 +- [ ] 加载状态有 spin/skeleton 提示 +- [ ] 错误有 message.error 提示 +- [ ] 提交按钮有 loading 防重复 + +## 安全 + +- [ ] 不在前端存储敏感信息(token 除外) +- [ ] 用户输入做 XSS 防护 +- [ ] 不拼接 URL 参数(使用 params) +- [ ] API 地址使用环境变量 + +## 代码质量 + +- [ ] 无 console.log 调试代码 +- [ ] 无注释掉的代码块 +- [ ] 无未使用的导入和变量 +- [ ] 模板中无复杂表达式 diff --git a/.agents/skills/command-executor/SKILL.md b/.agents/skills/command-executor/SKILL.md new file mode 100644 index 0000000..92c7200 --- /dev/null +++ b/.agents/skills/command-executor/SKILL.md @@ -0,0 +1,526 @@ +````markdown +--- +name: 命令执行与错误处理规范 +description: 命令执行规范(防阻塞、跨平台、超时)+ 错误分类、处理策略与升级路径,所有执行命令或处理错误的Agent均需遵循 +--- + +# 目的与整合说明 + +本 Skill 整合**命令执行规范**与**错误处理规范**,统一处理执行与错误问题。 + +- **命令执行**:定义跨操作系统的命令执行规范,防止阻塞命令导致系统无限等待,确保命令在不同平台上正确执行。 +- **错误处理**:定义统一的错误分类、记录格式、处理策略与升级路径,确保各角色在遇到问题时有清晰的处理策略。 + +**核心原则:防阻塞、跨平台、可超时;错误可分类、可追溯、可升级** + +> 任何终端命令执行前必须评估阻塞风险和系统兼容性。 +> +> **铁律:NO BLOCKING COMMANDS WITHOUT TIMEOUT OR BACKGROUND MODE** + +# 适用场景 +- 所有需要执行终端命令的 Agent +- 测试执行、构建运行、服务启动 +- 文件操作、环境检查、依赖安装 +- 任何 Agent 执行过程中遇到错误、需决定重试或升级策略 +- 需要记录和追踪问题 + +# 操作系统检测(必须首先执行) + +## 检测方法 + +在执行任何系统相关命令前,**必须先检测操作系统**: + +```bash +# 通用检测方法 +uname -s 2>/dev/null || echo "Windows" +``` + +### 检测结果与平台映射 + +| uname 输出 | 平台 | 变量设置 | +|-----------|------|----------| +| Darwin | macOS | `OS_TYPE=macos` | +| Linux | Linux | `OS_TYPE=linux` | +| MINGW* / MSYS* / CYGWIN* | Windows (Git Bash) | `OS_TYPE=windows` | +| 命令失败 | Windows (CMD/PowerShell) | `OS_TYPE=windows` | + +## 平台特定命令对照表 + +### 文件系统操作 + +| 操作 | macOS/Linux | Windows (CMD) | Windows (PowerShell) | +|------|-------------|---------------|----------------------| +| 列出文件 | `ls -la` | `dir` | `Get-ChildItem` | +| 查看文件 | `cat file` | `type file` | `Get-Content file` | +| 删除文件 | `rm file` | `del file` | `Remove-Item file` | +| 删除目录 | `rm -rf dir` | `rmdir /s /q dir` | `Remove-Item -Recurse dir` | +| 创建目录 | `mkdir -p dir` | `mkdir dir` | `New-Item -ItemType Directory dir` | +| 复制文件 | `cp src dst` | `copy src dst` | `Copy-Item src dst` | +| 移动文件 | `mv src dst` | `move src dst` | `Move-Item src dst` | +| 查找文件 | `find . -name "*.txt"` | `dir /s *.txt` | `Get-ChildItem -Recurse -Filter *.txt` | +| 搜索内容 | `grep "text" file` | `findstr "text" file` | `Select-String "text" file` | + +### 进程和网络操作 + +| 操作 | macOS | Linux | Windows | +|------|-------|-------|---------| +| 查看端口占用 | `lsof -i :PORT` | `ss -tlnp \| grep PORT` 或 `netstat -tlnp` | `netstat -ano \| findstr PORT` | +| 杀死进程 | `kill -9 PID` | `kill -9 PID` | `taskkill /F /PID PID` | +| 按名称杀进程 | `pkill -f name` | `pkill -f name` | `taskkill /F /IM name.exe` | +| 查看进程 | `ps aux` | `ps aux` | `tasklist` | +| 环境变量 | `export VAR=val` | `export VAR=val` | `set VAR=val` (CMD) / `$env:VAR=val` (PS) | +| 路径分隔符 | `/` | `/` | `\` (但 `/` 在大多数工具中也可用) | + +### 包管理器 + +| 平台 | 包管理器 | 安装示例 | +|------|---------|----------| +| macOS | Homebrew | `brew install jq` | +| Ubuntu/Debian | apt | `sudo apt install jq` | +| CentOS/RHEL | yum/dnf | `sudo yum install jq` | +| Windows | Chocolatey | `choco install jq` | +| Windows | Scoop | `scoop install jq` | +| Windows | winget | `winget install jqlang.jq` | + +# 阻塞命令处理(核心规范) + +## 阻塞命令识别 + +### 高风险阻塞命令列表 + +| 类别 | 命令示例 | 阻塞原因 | +|------|----------|----------| +| **服务器启动** | `npm run dev`, `npm start`, `cargo run`, `python -m http.server`, `go run main.go`, `rails server`, `flask run` | 持续运行,等待请求 | +| **监听模式** | `npm test --watch`, `cargo watch`, `tsc --watch`, `nodemon` | 持续监听文件变化 | +| **持续测试** | `cargo test` (有些情况), `pytest` (某些插件), `jest --watch` | 可能等待输入或持续运行 | +| **交互式命令** | `npm init`, `git commit` (无 -m), `ssh`, `mysql`, `psql` | 等待用户输入 | +| **长时间操作** | `docker build` (大型镜像), `npm install` (大型项目), `cargo build` (首次编译) | 可能超时 | +| **网络等待** | `curl` (慢服务), `wget` (大文件), `git clone` (大仓库) | 网络延迟 | + +### 阻塞检测规则 + +``` +IF 命令包含以下模式 THEN 标记为潜在阻塞: + - "run dev" | "run start" | "run serve" + - "server" | "serve" | "listen" + - "--watch" | "-w" (监听标志) + - "nodemon" | "supervisor" | "pm2 start" + - 无参数的 "cargo run" | "go run" | "python app.py" + - 数据库客户端命令(mysql, psql, mongo, redis-cli) + +IF 命令可能需要用户输入 THEN 标记为潜在阻塞: + - "npm init" (无 -y) + - "git commit" (无 -m) + - "ssh" (无密钥配置) +``` + +## 阻塞命令处理策略 + +### 策略1:后台模式启动(推荐用于服务器) + +```bash +# macOS/Linux +nohup npm run dev > /tmp/server.log 2>&1 & +echo $! > /tmp/server.pid +sleep 2 # 等待启动 +cat /tmp/server.log | head -20 # 查看启动日志 + +# 验证是否启动成功 +if curl -s http://localhost:3000/health > /dev/null; then + echo "服务器启动成功" +else + echo "服务器启动失败,查看日志:" + cat /tmp/server.log +fi +``` + +```powershell +# Windows PowerShell +Start-Process -NoNewWindow -FilePath "npm" -ArgumentList "run dev" -RedirectStandardOutput "server.log" +Start-Sleep -Seconds 2 +Get-Content server.log -Head 20 +``` + +### 策略2:超时控制(推荐用于测试和构建) + +```bash +# macOS/Linux - 使用 timeout 命令 +timeout 300 npm test # 5分钟超时 +timeout 600 cargo build # 10分钟超时 + +# macOS 可能需要 gtimeout (brew install coreutils) +gtimeout 300 npm test + +# 或使用后台 + 等待 +npm test & +PID=$! +sleep 300 && kill $PID 2>/dev/null & # 5分钟后杀死 +wait $PID +EXIT_CODE=$? +``` + +```powershell +# Windows PowerShell +$job = Start-Job { npm test } +$result = Wait-Job $job -Timeout 300 +if ($result.State -eq 'Running') { + Stop-Job $job + Write-Host "测试超时" +} else { + Receive-Job $job +} +``` + +### 策略3:非交互模式(推荐用于安装和初始化) + +```bash +# 使用 -y 或 --yes 跳过交互 +npm init -y +yarn init -y +pip install package --yes +apt install package -y + +# Git 提交必须带消息 +git commit -m "commit message" + +# 使用 expect 处理必须交互的情况(最后手段) +``` + +### 策略4:预检查 + 条件执行 + +```bash +# 检查端口是否被占用,避免启动失败阻塞 +check_port() { + local port=$1 + if [ "$(uname)" = "Darwin" ]; then + lsof -i :$port > /dev/null 2>&1 + elif [ "$(uname)" = "Linux" ]; then + ss -tlnp | grep ":$port " > /dev/null 2>&1 + else + netstat -ano | findstr ":$port " > /dev/null 2>&1 + fi +} + +if check_port 3000; then + echo "端口 3000 已被占用,请先关闭占用进程" + exit 1 +fi + +# 安全启动 +nohup npm run dev & +``` + +## 超时时间建议 + +| 操作类型 | 建议超时 | 说明 | +|----------|----------|------| +| 单元测试 | 5 分钟 | 单个测试文件 | +| 集成测试 | 10 分钟 | 完整测试套件 | +| 构建 (增量) | 5 分钟 | 增量编译 | +| 构建 (完整) | 15 分钟 | 首次或完整构建 | +| 依赖安装 | 10 分钟 | npm/pip/cargo install | +| 服务器启动 | 30 秒 | 等待服务就绪 | +| 网络请求 | 30 秒 | curl/wget 单次请求 | + +# 命令执行检查清单 + +## 执行前检查(必须) + +```markdown +- [ ] 已检测操作系统类型 +- [ ] 命令兼容当前操作系统 +- [ ] 已评估阻塞风险 +- [ ] 阻塞命令已添加超时或后台处理 +- [ ] 交互式命令已添加非交互参数 +- [ ] 路径分隔符正确 +``` + +## 命令模板 + +### 安全执行命令的通用模板 + +```bash +#!/bin/bash +# 命令执行安全模板 + +# 1. 检测操作系统 +detect_os() { + case "$(uname -s)" in + Darwin) echo "macos" ;; + Linux) echo "linux" ;; + MINGW*|MSYS*|CYGWIN*) echo "windows" ;; + *) echo "unknown" ;; + esac +} + +OS_TYPE=$(detect_os) +echo "检测到操作系统: $OS_TYPE" + +# 2. 设置超时命令 +if [ "$OS_TYPE" = "macos" ]; then + # macOS 需要 coreutils + TIMEOUT_CMD="gtimeout" + if ! command -v gtimeout &> /dev/null; then + TIMEOUT_CMD="perl -e 'alarm shift; exec @ARGV' --" + fi +else + TIMEOUT_CMD="timeout" +fi + +# 3. 执行命令示例(带超时) +echo "执行测试..." +$TIMEOUT_CMD 300 npm test +EXIT_CODE=$? + +if [ $EXIT_CODE -eq 124 ]; then + echo "命令执行超时" + exit 1 +elif [ $EXIT_CODE -ne 0 ]; then + echo "命令执行失败,退出码: $EXIT_CODE" + exit $EXIT_CODE +fi + +echo "命令执行成功" +``` + +# 常见问题场景处理 + +## 场景1:启动开发服务器并测试 + +**错误做法**(会阻塞): +```bash +npm run dev # 阻塞!永远不会返回 +curl http://localhost:3000 # 永远不会执行 +``` + +**正确做法**: +```bash +# 后台启动 +nohup npm run dev > /tmp/dev-server.log 2>&1 & +SERVER_PID=$! + +# 等待服务就绪(最多 30 秒) +for i in {1..30}; do + if curl -s http://localhost:3000 > /dev/null 2>&1; then + echo "服务器就绪" + break + fi + sleep 1 +done + +# 执行测试 +curl http://localhost:3000/api/test + +# 完成后清理 +kill $SERVER_PID 2>/dev/null +``` + +## 场景2:运行可能超时的测试 + +**错误做法**(可能无限等待): +```bash +cargo test # 某些测试可能卡住 +``` + +**正确做法**: +```bash +# 使用超时 +timeout 300 cargo test -- --test-threads=1 2>&1 | tee test-output.log +EXIT_CODE=${PIPESTATUS[0]} + +if [ $EXIT_CODE -eq 124 ]; then + echo "测试超时,查看日志: test-output.log" + exit 1 +fi +``` + +## 场景3:安装依赖 + +**错误做法**(可能请求输入): +```bash +npm init # 会请求用户输入 +``` + +**正确做法**: +```bash +npm init -y # 使用默认值 +npm install --yes # 自动确认 +pip install -r requirements.txt --no-input +``` + +## 场景4:检查端口(跨平台) + +```bash +check_port_in_use() { + local port=$1 + case "$(uname -s)" in + Darwin) + lsof -i :$port -sTCP:LISTEN > /dev/null 2>&1 + ;; + Linux) + ss -tlnp 2>/dev/null | grep -q ":$port " || \ + netstat -tlnp 2>/dev/null | grep -q ":$port " + ;; + *) # Windows + netstat -ano 2>/dev/null | grep -q ":$port " + ;; + esac +} + +if check_port_in_use 3000; then + echo "端口 3000 已被占用" +else + echo "端口 3000 可用" +fi +``` + +# 错误处理 + +## 命令执行失败 + +```bash +# 捕获退出码和输出 +OUTPUT=$(command 2>&1) +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + echo "命令失败 (退出码: $EXIT_CODE)" + echo "错误信息: $OUTPUT" + # 记录到错误日志 + echo "[$(date)] 命令失败: $EXIT_CODE - $OUTPUT" >> /tmp/errors.log +fi +``` + +## 超时处理 + +```bash +timeout 60 long_running_command +if [ $? -eq 124 ]; then + echo "命令超时,可能原因:" + echo "1. 命令执行时间超过预期" + echo "2. 命令陷入无限循环" + echo "3. 网络请求无响应" + # 清理可能遗留的进程 + pkill -f "long_running_command" 2>/dev/null +fi +``` + +# 错误分类与处理策略 + +## 按严重程度 + +| 级别 | 代码 | 含义 | 默认处理 | +|------|------|------|----------| +| CRITICAL | E1 | 系统级错误,无法继续 | 立即停止,上报人类 | +| HIGH | E2 | 阻塞当前任务 | 重试 1 次,失败则升级 | +| MEDIUM | E3 | 影响部分功能 | 重试 2 次,失败则记录继续 | +| LOW | E4 | 轻微问题 | 记录并继续 | + +## 按错误类型 + +| 类型 | 代码 | 描述 | 示例 | +|------|------|------|------| +| EXECUTION | T1 | 命令执行失败 | 编译错误、测试失败 | +| TIMEOUT | T2 | 操作超时 | 网络超时、构建超时 | +| RESOURCE | T3 | 资源问题 | 文件不存在、权限不足 | +| LOGIC | T4 | 逻辑错误 | 方案不可行、设计缺陷 | +| DEPENDENCY | T5 | 依赖问题 | 缺少前置文档、等待他人 | +| **BLOCKING** | T6 | **命令阻塞** | **服务器启动、监听模式、无限等待** | +| **PLATFORM** | T7 | **平台不兼容** | **操作系统命令不兼容** | +| UNKNOWN | T9 | 未知错误 | 无法分类的错误 | + +## 错误记录格式 + +```markdown +## ERROR-{YYYYMMDD-HHMMSS} +- **级别**: CRITICAL | HIGH | MEDIUM | LOW +- **类型**: EXECUTION | TIMEOUT | RESOURCE | LOGIC | DEPENDENCY | BLOCKING | PLATFORM | UNKNOWN +- **角色**: {发生错误的 Agent} +- **任务**: {正在执行的任务} +- **错误信息**: {详细错误} +- **上下文**: {相关文件、命令等} +- **已尝试**: {已尝试的解决方法} +- **状态**: 待处理 | 处理中 | 已解决 | 已升级 +- **处理方式**: {最终如何处理} +``` + +## 处理策略 + +### 策略0:3次失败质疑架构 + +**铁律:同一问题修复3次仍失败,必须质疑架构** + +- 停止修复,标记为架构级问题,升级到技术总监/人类决策。 +- 禁止“再试一次”“这次换个方法”;必须记录已尝试方法并升级。 + +### 策略1:自动重试 + +适用于网络超时(T2)、临时执行失败(T1)、资源暂时不可用(T3)。最大重试 3 次,重试间隔指数退避(5s/15s/45s),重试前检查错误是否可能为临时性。 + +### 策略2:上下文补充后重试 + +适用于信息不足导致的失败。步骤:分析缺少什么信息 → 从文档或代码获取 → 补充上下文后重试。 + +### 策略3:升级处理 + +升级路径:开发专员→技术骨干→技术总监→人类;测试专员→测试总监→人类;产品经理→产品总监→人类。触发条件:重试 N 次仍失败、错误超出当前角色能力、需要决策或授权。 + +### 策略4:阻塞等待 + +适用于依赖其他任务(T5)、需人类决策、需外部资源。记录 BLOCKED-{ID}:等待什么、原因、预计解除条件、超时。 + +## 错误恢复检查清单 + +错误解决后:验证问题已解决;更新错误记录状态;检查遗留影响;如有价值则总结经验。 + +## 度量指标 + +| 指标 | 计算方式 | 告警阈值 | +|------|----------|----------| +| 错误率 | 错误数/总任务数 | > 20% | +| 重试成功率 | 重试成功数/总重试数 | < 50% | +| 平均解决时间 | 总解决时间/错误数 | > 30分钟 | +| 升级率 | 升级数/错误数 | > 30% | + +# 与其他 Skill 的集成 + +## 在测试专员中 +- 测试前检测系统类型 +- 测试命令必须带超时 +- 服务器启动使用后台模式 + +## 在开发专员中 +- 构建命令添加超时 +- 开发服务器后台启动 +- 清理僵尸进程 + +## 在 supervisor-fix 中 +- 所有测试循环命令带超时 +- 检测阻塞后触发熔断 +- 记录命令执行耗时 + +## 在 Supervisor 中 +- 监控所有 Agent 的错误 +- 统计错误率,触发熔断 +- 协调升级处理 + +## 在各角色 Skill 中 +- 按本规范分类错误 +- 按本规范选择处理策略 +- 按本规范记录错误 + +# 注意事项 + +- **所有命令执行前必须先检测操作系统** +- **潜在阻塞命令必须使用超时或后台模式** +- **交互式命令必须添加非交互参数** +- **服务器类命令必须后台启动,并验证就绪状态** +- **命令失败时记录详细错误信息** +- **定期清理后台进程,避免资源泄漏** +- **超时时间要合理,既不能太短导致误判,也不能太长导致等待** +- **错误记录要包含足够上下文,便于他人理解** +- **升级时要带上已尝试的方法,避免重复劳动** +- **严重错误必须立即处理,不能积压** +- **定期回顾错误记录,发现系统性问题** + +```` diff --git a/.agents/skills/developer/SKILL.md b/.agents/skills/developer/SKILL.md new file mode 100644 index 0000000..f7b5172 --- /dev/null +++ b/.agents/skills/developer/SKILL.md @@ -0,0 +1,170 @@ +--- +name: 开发专员 +description: 当需要实现功能、修复Bug、编写单元测试时使用 +--- + +# 目的 +按照详细设计编写功能代码,修复Bug(包括技术骨干分析后的问题),编写单元测试。 + +> 核心原则遵循 `skills/shared/PRINCIPLES.md` +> 编码与接口设计同时遵循其中的**设计原则**:参数默认即是最优、接口命名意图驱动、大声报错且自带解药。 + +# 适用场景 +- 需要开发新功能 +- 需要修复Bug +- 需要编写单元测试 +- 遇到疑难问题需要求助 + +# 职责边界 + +## 核心职责 +- **编码实现**:按详细设计编写功能代码 +- **测试编写**:编写单元测试和集成测试 +- **Bug修复**:按技术骨干根因分析修复问题 +- **代码提交**:提交审查并配合修改 + +## 求助边界 +遇到以下情况必须求助技术骨干: +- 尝试3种方法仍无法定位问题 +- 不确定实现方案的可行性 +- 涉及不熟悉的技术栈 +- 修复3次仍失败(可能架构问题) + +# 文档规范 + +## 读取的文档 +| 文档 | 路径 | 说明 | +|------|------|------| +| 详细设计 | `.vibe/docs/design/*.md` | 开发依据 | +| 代码规范 | `.vibe/docs/代码规范.md` | 编码标准 | +| 进度总览 | `.vibe/docs/进度总览.md` | 待办任务 | +| 问题跟踪 | `.vibe/docs/问题跟踪.md` | Bug全生命周期(分析、待修复、待验证) | + +## 输出的文档 +| 文档 | 路径 | 说明 | +|------|------|------| +| 问题跟踪 | `.vibe/docs/问题跟踪.md` | 求助、进度、修复状态统一写入 | +| 进度总览 | `.vibe/docs/进度总览.md` | 更新任务状态 | + +## 文档更新原则 +- 问题求助:尝试3种以上方法无法解决再求助 +- 开发进度:任务开始和完成时更新 + +# 工作流程 + +## 流程1:功能开发 +``` +输入:详细设计、开发任务 +输出:功能代码、单元测试 +``` +1. 阅读详细设计 +2. 编写功能代码 +3. 编写单元测试 +4. 自测通过后提交审查 +5. 更新开发进度 + +## 流程2:Bug修复 +``` +输入:问题跟踪 +输出:修复后的代码 +``` +1. 阅设Bug描述或问题分析 +2. 按建议修复代码 +3. 更新测试 +4. 提交审查 +5. 在 `.vibe/docs/问题跟踪.md` 更新状态为“待验证” + +## 流程3:问题求助 +``` +输入:疑难问题 +输出:.vibe/docs/问题跟踪.md(添加求助条目) +``` +当遇到以下情况求助技术骨干: +- 尝试3种以上方法仍无法定位 +- 不确定实现方案 +- 涉及不熟悉技术 + +# 协作接口 + +## 向技术骨干求助 +`.vibe/docs/问题跟踪.md`(添加求助条目): +```markdown +## 求助: {标题} +- 描述: {详情} +- 已尝试: {尝试过的方法} +- 代码: {文件路径和行号} +- 错误: {如有} +- 状态: 待分析 +``` + +## 更新任务状态 +`.vibe/docs/进度总览.md`: +```markdown +## {任务} +- 状态: 进行中/已完成/阻塞 +- 进度: {说明} +``` + +## 通知测试验证 +`.vibe/docs/问题跟踪.md`(更新状态为待验证): +```markdown +## BUG-{xxx}: 状态更新为“待验证” +- 修复: {说明} +``` + +# Bug修复规范 + +## 根因分析先行 + +**修复前必须确认:** +1. 阅读 `.vibe/docs/问题跟踪.md` 中的根因分析 +2. 理解问题产生的根本原因(不是表面症状) +3. 确认修复方案针对的是根因 +4. 如果不理解分析,先求助技术骨干澄清 + +## 禁止的修复方式(Workaround) + +以下行为属于 Workaround,**严禁使用**: +- ❌ 脚本报错后手动创建空文件/假数据 +- ❌ 跳过失败的步骤继续执行 +- ❌ 注释掉失败的测试用例 +- ❌ 用硬编码值绕过动态逻辑 +- ❌ 捕获异常但不处理(空 catch) +- ❌ 修改输出路径而非修复逻辑 +- ❌ 返回默认值而非修复逻辑 + +## 3次失败原则 + +如果同一 Bug 修复 3 次仍未解决: +1. STOP 继续修复尝试 +2. 在 `.vibe/docs/问题跟踪.md` 中记录: + - 已尝试的修复方法 + - 每次失败的原因 + - 怀疑的架构问题 +3. 请求技术骨干重新分析(可能是架构问题) + +## 红旗信号 - STOP + +如果发现自己想要: +- "先这样绕过,以后再说" +- "注释掉这个测试试试" +- "返回个默认值应该可以" +- "不太理解但先这样改" + +**STOP。回到问题分析,理解根因后再修复。** + +## 修复验证清单 + +修复完成后,验证: +- [ ] 修复针对的是根因而非症状 +- [ ] 没有使用任何 Workaround 方式 +- [ ] 原有测试通过 +- [ ] 新增回归测试(如适用) +- [ ] 没有破坏其他功能 + +# 注意事项 +- 遇难题先尝试3种以上方法,无法解决再求助 +- **按技术骨干分析建议修复,不要自行发挥** +- 修复后必须通知测试验证 +- **核心原则遵循 `skills/shared/PRINCIPLES.md`** +- **命令执行遵循 `skills/command-executor/SKILL.md`** diff --git a/.agents/skills/fix-and-lint/SKILL.md b/.agents/skills/fix-and-lint/SKILL.md new file mode 100644 index 0000000..dc8ac33 --- /dev/null +++ b/.agents/skills/fix-and-lint/SKILL.md @@ -0,0 +1,80 @@ +--- +name: fix-and-lint +description: | + 修复代码格式和 lint 问题。当用户在提交前需要修复代码风格、 + 排查 lint 错误、或说 "fix"、"格式化"、"lint" 时激活。 +--- + +# Fix and Lint + +## 何时使用 + +- 提交前修复代码格式 +- lint 检查有报错 +- 用户说 "fix"、"格式化"、"lint 修复" + +## 后端 (Python) + +### 步骤 + +```bash +# 1. 格式化 +cd backend && uv run ruff format . + +# 2. Lint 修复 +cd backend && uv run ruff check --fix . + +# 3. 检查剩余问题 +cd backend && uv run ruff check . +``` + +### 常见问题修复 + +| 问题 | 修复方式 | +|------|---------| +| 未使用的导入 | `ruff check --fix` 自动移除 | +| 行过长 | `ruff format` 自动换行 | +| 导入顺序 | `ruff check --fix` 自动排序 | +| 类型注解缺失 | 手动添加 | + +### 验证 + +```bash +# 确保修复后测试仍通过 +cd backend && uv run pytest +``` + +## 前端 (TypeScript/Vue) + +### 步骤 + +```bash +# 1. 格式化 +cd frontend && pnpm run format + +# 2. Lint 修复 +cd frontend && pnpm run lint:fix + +# 3. 检查剩余问题 +cd frontend && pnpm run lint +``` + +## 数据库迁移检查 + +如果修改了 model 文件: + +```bash +# 检查是否有未应用的迁移 +cd backend && uv run alembic check + +# 生成迁移(如有模型变更) +cd backend && uv run alembic revision --autogenerate -m "描述" +``` + +## 输出 + +修复完成后报告: +1. 格式化了哪些文件 +2. 修复了哪些 lint 问题 +3. 仍需手动修复的问题(如有) +4. 测试是否通过 diff --git a/.agents/skills/frontend-development/SKILL.md b/.agents/skills/frontend-development/SKILL.md new file mode 100644 index 0000000..61914b1 --- /dev/null +++ b/.agents/skills/frontend-development/SKILL.md @@ -0,0 +1,238 @@ +--- +name: frontend-development +description: | + MiniHES 前端开发规范。当用户进行 Vue 3 前端开发、组件设计、 + 页面布局、样式调整时自动激活。 + 涵盖技术栈、组件设计原则、设计规范、Ant Design Vue 用法。 +--- + +# MiniHES 前端开发规范 + +## 何时使用 + +- 编写 Vue 3 组件、页面、路由 +- 使用 Ant Design Vue 组件 +- 调整样式、布局、交互 +- 前端状态管理 +- API 请求封装 + +## 技术栈 + +- **框架**: Vue 3 + TypeScript +- **UI 库**: Ant Design Vue +- **构建**: Vite +- **状态管理**: Pinia +- **路由**: Vue Router +- **HTTP**: 基于 @vben/request 封装 +- **脚手架**: vue-vben-admin 5.7.0 + +## 目录结构 + +``` +apps/web-antd/src/ +├── api/ # API 请求封装 +├── views/ # 页面组件(按业务模块) +│ ├── devices/ # 设备管理 +│ ├── collector/ # 数据采集 +│ ├── analysis/ # 分析报表 +│ └── system/ # 系统管理 +├── components/ # 全局共享组件 +├── stores/ # Pinia stores +├── router/ # 路由配置 +├── locales/ # 国际化 +└── utils/ # 工具函数 +``` + +## 组件设计原则 + +### 命名 + +| 类型 | 规范 | 示例 | +|------|------|------| +| 页面 | PascalCase | `MeterList.vue`, `TaskDetail.vue` | +| 组件 | PascalCase + 业务前缀 | `MeterStatusTag.vue` | +| Composable | use + 功能 | `useMeterList.ts` | +| Store | use + 模块 + Store | `useMeterStore.ts` | +| API 文件 | 模块名小写 | `meter.ts`, `task.ts` | + +### 组件结构顺序 + +```vue + + + + + +``` + +### 组件设计规则 + +- 单一职责:一个组件做一件事 +- Props 向下,Events 向上 +- 复杂逻辑提取为 composable +- 列表渲染必须用 `:key` +- 条件渲染用 `v-show`(频繁切换)或 `v-if`(条件分支) + +## API 封装 + +```typescript +// api/meter.ts +import { requestClient } from '#/api/request'; + +export async function getMeterList(params: MeterListParams) { + return requestClient.get('/api/v1/meters', { params }); +} + +export async function createMeter(data: CreateMeterRequest) { + return requestClient.post('/api/v1/meters', data); +} +``` + +### 规则 + +- 每个 API 文件对应一个后端模块 +- 使用 TypeScript 接口定义请求/响应类型 +- 统一使用 `requestClient`,不直接用 axios +- 错误处理在 `requestClient` 层统一处理 + +## 状态管理 + +### 何时用 Store + +| 场景 | 方案 | +|------|------| +| 组件内状态 | `ref` / `reactive` | +| 跨组件共享 | Pinia Store | +| 服务端数据 | API 直接请求 + 组件内缓存 | +| 全局配置 | Store | + +### Store 结构 + +```typescript +// stores/meter.ts +import { defineStore } from 'pinia'; + +export const useMeterStore = defineStore('meter', () => { + const meters = ref([]); + const loading = ref(false); + + async function fetchMeters(params?: MeterListParams) { + loading.value = true; + try { + const res = await getMeterList(params); + meters.value = res.items; + } finally { + loading.value = false; + } + } + + return { meters, loading, fetchMeters }; +}); +``` + +## 设计思维 + +开始编码前,先明确四个维度: + +1. **Purpose**: 这个页面/组件解决什么问题?谁使用? +2. **Tone**: 选择一个明确的设计调性(工业感/数据仪表盘风/极简/专业严肃),全局保持一致 +3. **Constraints**: 技术约束(框架、性能、无障碍) +4. **Differentiation**: 这个界面有什么让人记住的?一个清晰的数据可视化?一个流畅的交互? + +**核心原则**: 选择一个明确的设计方向并精确执行。大胆极繁和精致极简都可行——关键是**有意图性**。 + +## 设计规范 + +参见 [design-checklist.md](references/design-checklist.md)。 + +### 排版与字体 + +- 选择有辨识度的字体组合,不要用 Inter/Roboto/Arial 等通用字体 +- 数据面板用等宽字体(如 JetBrains Mono、Fira Code) +- 标题用有特色的 display font,正文用清晰的 UI font +- 字重对比:标题 600-700,正文 400,辅助文字 300 + +### 色彩与主题 + +- 主色跟随 Ant Design Vue 主题 token,不硬编码色值 +- 使用 CSS 变量保持一致性 +- 用深浅对比创造层次感,而不是均匀分布色彩 +- 状态色统一:成功绿、失败红、进行中蓝、警告橙 +- 禁止淡紫色渐变+白色背景这种 AI 通用配色 + +### 动效与微交互 + +- 页面加载使用错开显示(staggered reveal with animation-delay) +- 列表项、卡片入场动画增加层次感 +- 悬停状态要有反馈(阴影变化、色彩加深、微位移) +- 数据刷新使用过渡动画 +- 优先使用 CSS-only 方案,复杂交互用 Vue transition + +### 空间构图 + +- 不总是使用对称网格,关键数据可以突出展示 +- 利用重叠、留白、对角线引导视线 +- 数据面板和仪表盘允许控制性密度 +- 重要操作区域有足够的呼吸空间 + +### 背景与视觉细节 + +- 不使用纯色背景,加入细微纹理(噪点、网格线、渐变) +- 卡片用微妙阴影和边框增加深度 +- 数据区域可以用几何图案或渐变网格 +- 状态指示器使用发光效果或渐变填充 + +### 交互规范 + +- 加载状态:使用 `a-spin` 或骨架屏 +- 空状态:使用 `a-empty` +- 错误提示:使用 `message.error()` +- 成功提示:使用 `message.success()` +- 删除操作:必须二次确认 `Modal.confirm()` + +### 表单 + +- 使用 Ant Design Form 组件 +- 必填字段标注 `*` +- 实时验证 (`validateTrigger: 'change'`) +- 提交时全量验证 +- 提交按钮 loading 状态防止重复 + +## 输出格式 + +构建前端功能时,按以下格式输出: + +1. **文件结构** — 展示文件应放在哪里 +2. **完整代码** — 功能完整、有类型注解的代码 +3. **依赖** — 需要安装的包 +4. **环境变量** — 如有需要 +5. **运行说明** — 如何启动和验证 + +## 性能优化 + +- 路由懒加载 (`() => import(...)`) +- 大列表用虚拟滚动 +- 图片懒加载 +- 避免不必要的响应式 (`shallowRef` / `markRaw`) +- 组件 `v-memo` 减少重渲染 + +## 禁止 + +- 直接操作 DOM(除非必要) +- 在模板中写复杂表达式 +- 全局污染样式(必须 `scoped`) +- 硬编码 API 地址(使用环境变量) +- 提交 `console.log` 调试代码 diff --git a/.agents/skills/frontend-development/references/design-checklist.md b/.agents/skills/frontend-development/references/design-checklist.md new file mode 100644 index 0000000..d183c97 --- /dev/null +++ b/.agents/skills/frontend-development/references/design-checklist.md @@ -0,0 +1,59 @@ +# UI 设计检查清单 + +## 布局 + +- [ ] 页面有明确的视觉层次(标题 > 副标题 > 内容) +- [ ] 内容区域宽度合理,不超出 1200px 居中或满宽栅格 +- [ ] 操作按钮位置一致(右上角 / 表格上方) +- [ ] 响应式适配(至少支持 1280px+ 宽度) +- [ ] 面包屑导航与路由对应 + +## 表格 + +- [ ] 列宽合理,重要信息在左侧 +- [ ] 操作列固定在右侧 +- [ ] 状态列用 Tag/Badge 可视化 +- [ ] 空数据有友好提示 +- [ ] 数据量大时有分页 +- [ ] 支持按列排序和筛选(业务需要的列) + +## 表单 + +- [ ] 标签简洁明确 +- [ ] 必填项有 `*` 标记 +- [ ] 输入框宽度与预期内容匹配 +- [ ] 下拉选项数量 > 5 时支持搜索 +- [ ] 日期选择器限制合理范围 +- [ ] 提交按钮有 loading 状态 +- [ ] 取消操作有确认(已填写内容时) + +## 弹窗 / 抽屉 + +- [ ] 标题清晰描述操作 +- [ ] 宽度与内容匹配(表单用抽屉,详情用弹窗) +- [ ] 关闭前有确认(未保存的变更) +- [ ] 操作按钮在底部右对齐 + +## 反馈 + +- [ ] 加载中有 spin / skeleton 提示 +- [ ] 操作成功有 message.success +- [ ] 操作失败有 message.error + 原因 +- [ ] 删除 / 危险操作有二次确认 +- [ ] 批量操作显示进度 + +## 数据展示 + +- [ ] 数值有合理的精度(电能 2 位小数,百分比 1 位) +- [ ] 大数字有千分位分隔 +- [ ] 时间显示友好格式 +- [ ] 状态有颜色区分(成功绿、失败红、进行中蓝、警告橙) +- [ ] 图表有图例和单位标注 + +## 空间与间距 + +- [ ] 统一使用 8px 基础间距倍数 +- [ ] 卡片之间有 16px 间距 +- [ ] 表单字段之间有 16-24px 间距 +- [ ] 页面顶部有 16-24px 呼吸空间 +- [ ] 操作按钮组之间有 8px 间距 diff --git a/.agents/skills/hermes-api-design/SKILL.md b/.agents/skills/hermes-api-design/SKILL.md new file mode 100644 index 0000000..a45aca0 --- /dev/null +++ b/.agents/skills/hermes-api-design/SKILL.md @@ -0,0 +1,523 @@ +--- +name: api-design +description: REST API design patterns including resource naming, status codes, pagination, filtering, error responses, versioning, and rate limiting for production APIs. +origin: ECC +--- + +# API Design Patterns + +Conventions and best practices for designing consistent, developer-friendly REST APIs. + +## When to Activate + +- Designing new API endpoints +- Reviewing existing API contracts +- Adding pagination, filtering, or sorting +- Implementing error handling for APIs +- Planning API versioning strategy +- Building public or partner-facing APIs + +## Resource Design + +### URL Structure + +``` +# Resources are nouns, plural, lowercase, kebab-case +GET /api/v1/users +GET /api/v1/users/:id +POST /api/v1/users +PUT /api/v1/users/:id +PATCH /api/v1/users/:id +DELETE /api/v1/users/:id + +# Sub-resources for relationships +GET /api/v1/users/:id/orders +POST /api/v1/users/:id/orders + +# Actions that don't map to CRUD (use verbs sparingly) +POST /api/v1/orders/:id/cancel +POST /api/v1/auth/login +POST /api/v1/auth/refresh +``` + +### Naming Rules + +``` +# GOOD +/api/v1/team-members # kebab-case for multi-word resources +/api/v1/orders?status=active # query params for filtering +/api/v1/users/123/orders # nested resources for ownership + +# BAD +/api/v1/getUsers # verb in URL +/api/v1/user # singular (use plural) +/api/v1/team_members # snake_case in URLs +/api/v1/users/123/getOrders # verb in nested resource +``` + +## HTTP Methods and Status Codes + +### Method Semantics + +| Method | Idempotent | Safe | Use For | +|--------|-----------|------|---------| +| GET | Yes | Yes | Retrieve resources | +| POST | No | No | Create resources, trigger actions | +| PUT | Yes | No | Full replacement of a resource | +| PATCH | No* | No | Partial update of a resource | +| DELETE | Yes | No | Remove a resource | + +*PATCH can be made idempotent with proper implementation + +### Status Code Reference + +``` +# Success +200 OK — GET, PUT, PATCH (with response body) +201 Created — POST (include Location header) +204 No Content — DELETE, PUT (no response body) + +# Client Errors +400 Bad Request — Validation failure, malformed JSON +401 Unauthorized — Missing or invalid authentication +403 Forbidden — Authenticated but not authorized +404 Not Found — Resource doesn't exist +409 Conflict — Duplicate entry, state conflict +422 Unprocessable Entity — Semantically invalid (valid JSON, bad data) +429 Too Many Requests — Rate limit exceeded + +# Server Errors +500 Internal Server Error — Unexpected failure (never expose details) +502 Bad Gateway — Upstream service failed +503 Service Unavailable — Temporary overload, include Retry-After +``` + +### Common Mistakes + +``` +# BAD: 200 for everything +{ "status": 200, "success": false, "error": "Not found" } + +# GOOD: Use HTTP status codes semantically +HTTP/1.1 404 Not Found +{ "error": { "code": "not_found", "message": "User not found" } } + +# BAD: 500 for validation errors +# GOOD: 400 or 422 with field-level details + +# BAD: 200 for created resources +# GOOD: 201 with Location header +HTTP/1.1 201 Created +Location: /api/v1/users/abc-123 +``` + +## Response Format + +### Success Response + +```json +{ + "data": { + "id": "abc-123", + "email": "alice@example.com", + "name": "Alice", + "created_at": "2025-01-15T10:30:00Z" + } +} +``` + +### Collection Response (with Pagination) + +```json +{ + "data": [ + { "id": "abc-123", "name": "Alice" }, + { "id": "def-456", "name": "Bob" } + ], + "meta": { + "total": 142, + "page": 1, + "per_page": 20, + "total_pages": 8 + }, + "links": { + "self": "/api/v1/users?page=1&per_page=20", + "next": "/api/v1/users?page=2&per_page=20", + "last": "/api/v1/users?page=8&per_page=20" + } +} +``` + +### Error Response + +```json +{ + "error": { + "code": "validation_error", + "message": "Request validation failed", + "details": [ + { + "field": "email", + "message": "Must be a valid email address", + "code": "invalid_format" + }, + { + "field": "age", + "message": "Must be between 0 and 150", + "code": "out_of_range" + } + ] + } +} +``` + +### Response Envelope Variants + +```typescript +// Option A: Envelope with data wrapper (recommended for public APIs) +interface ApiResponse { + data: T; + meta?: PaginationMeta; + links?: PaginationLinks; +} + +interface ApiError { + error: { + code: string; + message: string; + details?: FieldError[]; + }; +} + +// Option B: Flat response (simpler, common for internal APIs) +// Success: just return the resource directly +// Error: return error object +// Distinguish by HTTP status code +``` + +## Pagination + +### Offset-Based (Simple) + +``` +GET /api/v1/users?page=2&per_page=20 + +# Implementation +SELECT * FROM users +ORDER BY created_at DESC +LIMIT 20 OFFSET 20; +``` + +**Pros:** Easy to implement, supports "jump to page N" +**Cons:** Slow on large offsets (OFFSET 100000), inconsistent with concurrent inserts + +### Cursor-Based (Scalable) + +``` +GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20 + +# Implementation +SELECT * FROM users +WHERE id > :cursor_id +ORDER BY id ASC +LIMIT 21; -- fetch one extra to determine has_next +``` + +```json +{ + "data": [...], + "meta": { + "has_next": true, + "next_cursor": "eyJpZCI6MTQzfQ" + } +} +``` + +**Pros:** Consistent performance regardless of position, stable with concurrent inserts +**Cons:** Cannot jump to arbitrary page, cursor is opaque + +### When to Use Which + +| Use Case | Pagination Type | +|----------|----------------| +| Admin dashboards, small datasets (<10K) | Offset | +| Infinite scroll, feeds, large datasets | Cursor | +| Public APIs | Cursor (default) with offset (optional) | +| Search results | Offset (users expect page numbers) | + +## Filtering, Sorting, and Search + +### Filtering + +``` +# Simple equality +GET /api/v1/orders?status=active&customer_id=abc-123 + +# Comparison operators (use bracket notation) +GET /api/v1/products?price[gte]=10&price[lte]=100 +GET /api/v1/orders?created_at[after]=2025-01-01 + +# Multiple values (comma-separated) +GET /api/v1/products?category=electronics,clothing + +# Nested fields (dot notation) +GET /api/v1/orders?customer.country=US +``` + +### Sorting + +``` +# Single field (prefix - for descending) +GET /api/v1/products?sort=-created_at + +# Multiple fields (comma-separated) +GET /api/v1/products?sort=-featured,price,-created_at +``` + +### Full-Text Search + +``` +# Search query parameter +GET /api/v1/products?q=wireless+headphones + +# Field-specific search +GET /api/v1/users?email=alice +``` + +### Sparse Fieldsets + +``` +# Return only specified fields (reduces payload) +GET /api/v1/users?fields=id,name,email +GET /api/v1/orders?fields=id,total,status&include=customer.name +``` + +## Authentication and Authorization + +### Token-Based Auth + +``` +# Bearer token in Authorization header +GET /api/v1/users +Authorization: Bearer eyJhbGciOiJIUzI1NiIs... + +# API key (for server-to-server) +GET /api/v1/data +X-API-Key: sk_live_abc123 +``` + +### Authorization Patterns + +```typescript +// Resource-level: check ownership +app.get("/api/v1/orders/:id", async (req, res) => { + const order = await Order.findById(req.params.id); + if (!order) return res.status(404).json({ error: { code: "not_found" } }); + if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } }); + return res.json({ data: order }); +}); + +// Role-based: check permissions +app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => { + await User.delete(req.params.id); + return res.status(204).send(); +}); +``` + +## Rate Limiting + +### Headers + +``` +HTTP/1.1 200 OK +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 95 +X-RateLimit-Reset: 1640000000 + +# When exceeded +HTTP/1.1 429 Too Many Requests +Retry-After: 60 +{ + "error": { + "code": "rate_limit_exceeded", + "message": "Rate limit exceeded. Try again in 60 seconds." + } +} +``` + +### Rate Limit Tiers + +| Tier | Limit | Window | Use Case | +|------|-------|--------|----------| +| Anonymous | 30/min | Per IP | Public endpoints | +| Authenticated | 100/min | Per user | Standard API access | +| Premium | 1000/min | Per API key | Paid API plans | +| Internal | 10000/min | Per service | Service-to-service | + +## Versioning + +### URL Path Versioning (Recommended) + +``` +/api/v1/users +/api/v2/users +``` + +**Pros:** Explicit, easy to route, cacheable +**Cons:** URL changes between versions + +### Header Versioning + +``` +GET /api/users +Accept: application/vnd.myapp.v2+json +``` + +**Pros:** Clean URLs +**Cons:** Harder to test, easy to forget + +### Versioning Strategy + +``` +1. Start with /api/v1/ — don't version until you need to +2. Maintain at most 2 active versions (current + previous) +3. Deprecation timeline: + - Announce deprecation (6 months notice for public APIs) + - Add Sunset header: Sunset: Sat, 01 Jan 2026 00:00:00 GMT + - Return 410 Gone after sunset date +4. Non-breaking changes don't need a new version: + - Adding new fields to responses + - Adding new optional query parameters + - Adding new endpoints +5. Breaking changes require a new version: + - Removing or renaming fields + - Changing field types + - Changing URL structure + - Changing authentication method +``` + +## Implementation Patterns + +### TypeScript (Next.js API Route) + +```typescript +import { z } from "zod"; +import { NextRequest, NextResponse } from "next/server"; + +const createUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(1).max(100), +}); + +export async function POST(req: NextRequest) { + const body = await req.json(); + const parsed = createUserSchema.safeParse(body); + + if (!parsed.success) { + return NextResponse.json({ + error: { + code: "validation_error", + message: "Request validation failed", + details: parsed.error.issues.map(i => ({ + field: i.path.join("."), + message: i.message, + code: i.code, + })), + }, + }, { status: 422 }); + } + + const user = await createUser(parsed.data); + + return NextResponse.json( + { data: user }, + { + status: 201, + headers: { Location: `/api/v1/users/${user.id}` }, + }, + ); +} +``` + +### Python (Django REST Framework) + +```python +from rest_framework import serializers, viewsets, status +from rest_framework.response import Response + +class CreateUserSerializer(serializers.Serializer): + email = serializers.EmailField() + name = serializers.CharField(max_length=100) + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["id", "email", "name", "created_at"] + +class UserViewSet(viewsets.ModelViewSet): + serializer_class = UserSerializer + permission_classes = [IsAuthenticated] + + def get_serializer_class(self): + if self.action == "create": + return CreateUserSerializer + return UserSerializer + + def create(self, request): + serializer = CreateUserSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = UserService.create(**serializer.validated_data) + return Response( + {"data": UserSerializer(user).data}, + status=status.HTTP_201_CREATED, + headers={"Location": f"/api/v1/users/{user.id}"}, + ) +``` + +### Go (net/http) + +```go +func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) { + var req CreateUserRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body") + return + } + + if err := req.Validate(); err != nil { + writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error()) + return + } + + user, err := h.service.Create(r.Context(), req) + if err != nil { + switch { + case errors.Is(err, domain.ErrEmailTaken): + writeError(w, http.StatusConflict, "email_taken", "Email already registered") + default: + writeError(w, http.StatusInternalServerError, "internal_error", "Internal error") + } + return + } + + w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID)) + writeJSON(w, http.StatusCreated, map[string]any{"data": user}) +} +``` + +## API Design Checklist + +Before shipping a new endpoint: + +- [ ] Resource URL follows naming conventions (plural, kebab-case, no verbs) +- [ ] Correct HTTP method used (GET for reads, POST for creates, etc.) +- [ ] Appropriate status codes returned (not 200 for everything) +- [ ] Input validated with schema (Zod, Pydantic, Bean Validation) +- [ ] Error responses follow standard format with codes and messages +- [ ] Pagination implemented for list endpoints (cursor or offset) +- [ ] Authentication required (or explicitly marked as public) +- [ ] Authorization checked (user can only access their own resources) +- [ ] Rate limiting configured +- [ ] Response does not leak internal details (stack traces, SQL errors) +- [ ] Consistent naming with existing endpoints (camelCase vs snake_case) +- [ ] Documented (OpenAPI/Swagger spec updated) diff --git a/.agents/skills/hermes-backend-patterns/SKILL.md b/.agents/skills/hermes-backend-patterns/SKILL.md new file mode 100644 index 0000000..30898b4 --- /dev/null +++ b/.agents/skills/hermes-backend-patterns/SKILL.md @@ -0,0 +1,598 @@ +--- +name: backend-patterns +description: Backend architecture patterns, API design, database optimization, and server-side best practices for Node.js, Express, and Next.js API routes. +origin: ECC +--- + +# Backend Development Patterns + +Backend architecture patterns and best practices for scalable server-side applications. + +## When to Activate + +- Designing REST or GraphQL API endpoints +- Implementing repository, service, or controller layers +- Optimizing database queries (N+1, indexing, connection pooling) +- Adding caching (Redis, in-memory, HTTP cache headers) +- Setting up background jobs or async processing +- Structuring error handling and validation for APIs +- Building middleware (auth, logging, rate limiting) + +## API Design Patterns + +### RESTful API Structure + +```typescript +// PASS: Resource-based URLs +GET /api/markets # List resources +GET /api/markets/:id # Get single resource +POST /api/markets # Create resource +PUT /api/markets/:id # Replace resource +PATCH /api/markets/:id # Update resource +DELETE /api/markets/:id # Delete resource + +// PASS: Query parameters for filtering, sorting, pagination +GET /api/markets?status=active&sort=volume&limit=20&offset=0 +``` + +### Repository Pattern + +```typescript +// Abstract data access logic +interface MarketRepository { + findAll(filters?: MarketFilters): Promise + findById(id: string): Promise + create(data: CreateMarketDto): Promise + update(id: string, data: UpdateMarketDto): Promise + delete(id: string): Promise +} + +class SupabaseMarketRepository implements MarketRepository { + async findAll(filters?: MarketFilters): Promise { + let query = supabase.from('markets').select('*') + + if (filters?.status) { + query = query.eq('status', filters.status) + } + + if (filters?.limit) { + query = query.limit(filters.limit) + } + + const { data, error } = await query + + if (error) throw new Error(error.message) + return data + } + + // Other methods... +} +``` + +### Service Layer Pattern + +```typescript +// Business logic separated from data access +class MarketService { + constructor(private marketRepo: MarketRepository) {} + + async searchMarkets(query: string, limit: number = 10): Promise { + // Business logic + const embedding = await generateEmbedding(query) + const results = await this.vectorSearch(embedding, limit) + + // Fetch full data + const markets = await this.marketRepo.findByIds(results.map(r => r.id)) + + // Sort by similarity + return markets.sort((a, b) => { + const scoreA = results.find(r => r.id === a.id)?.score || 0 + const scoreB = results.find(r => r.id === b.id)?.score || 0 + return scoreA - scoreB + }) + } + + private async vectorSearch(embedding: number[], limit: number) { + // Vector search implementation + } +} +``` + +### Middleware Pattern + +```typescript +// Request/response processing pipeline +export function withAuth(handler: NextApiHandler): NextApiHandler { + return async (req, res) => { + const token = req.headers.authorization?.replace('Bearer ', '') + + if (!token) { + return res.status(401).json({ error: 'Unauthorized' }) + } + + try { + const user = await verifyToken(token) + req.user = user + return handler(req, res) + } catch (error) { + return res.status(401).json({ error: 'Invalid token' }) + } + } +} + +// Usage +export default withAuth(async (req, res) => { + // Handler has access to req.user +}) +``` + +## Database Patterns + +### Query Optimization + +```typescript +// PASS: GOOD: Select only needed columns +const { data } = await supabase + .from('markets') + .select('id, name, status, volume') + .eq('status', 'active') + .order('volume', { ascending: false }) + .limit(10) + +// FAIL: BAD: Select everything +const { data } = await supabase + .from('markets') + .select('*') +``` + +### N+1 Query Prevention + +```typescript +// FAIL: BAD: N+1 query problem +const markets = await getMarkets() +for (const market of markets) { + market.creator = await getUser(market.creator_id) // N queries +} + +// PASS: GOOD: Batch fetch +const markets = await getMarkets() +const creatorIds = markets.map(m => m.creator_id) +const creators = await getUsers(creatorIds) // 1 query +const creatorMap = new Map(creators.map(c => [c.id, c])) + +markets.forEach(market => { + market.creator = creatorMap.get(market.creator_id) +}) +``` + +### Transaction Pattern + +```typescript +async function createMarketWithPosition( + marketData: CreateMarketDto, + positionData: CreatePositionDto +) { + // Use Supabase transaction + const { data, error } = await supabase.rpc('create_market_with_position', { + market_data: marketData, + position_data: positionData + }) + + if (error) throw new Error('Transaction failed') + return data +} + +// SQL function in Supabase +CREATE OR REPLACE FUNCTION create_market_with_position( + market_data jsonb, + position_data jsonb +) +RETURNS jsonb +LANGUAGE plpgsql +AS $$ +BEGIN + -- Start transaction automatically + INSERT INTO markets VALUES (market_data); + INSERT INTO positions VALUES (position_data); + RETURN jsonb_build_object('success', true); +EXCEPTION + WHEN OTHERS THEN + -- Rollback happens automatically + RETURN jsonb_build_object('success', false, 'error', SQLERRM); +END; +$$; +``` + +## Caching Strategies + +### Redis Caching Layer + +```typescript +class CachedMarketRepository implements MarketRepository { + constructor( + private baseRepo: MarketRepository, + private redis: RedisClient + ) {} + + async findById(id: string): Promise { + // Check cache first + const cached = await this.redis.get(`market:${id}`) + + if (cached) { + return JSON.parse(cached) + } + + // Cache miss - fetch from database + const market = await this.baseRepo.findById(id) + + if (market) { + // Cache for 5 minutes + await this.redis.setex(`market:${id}`, 300, JSON.stringify(market)) + } + + return market + } + + async invalidateCache(id: string): Promise { + await this.redis.del(`market:${id}`) + } +} +``` + +### Cache-Aside Pattern + +```typescript +async function getMarketWithCache(id: string): Promise { + const cacheKey = `market:${id}` + + // Try cache + const cached = await redis.get(cacheKey) + if (cached) return JSON.parse(cached) + + // Cache miss - fetch from DB + const market = await db.markets.findUnique({ where: { id } }) + + if (!market) throw new Error('Market not found') + + // Update cache + await redis.setex(cacheKey, 300, JSON.stringify(market)) + + return market +} +``` + +## Error Handling Patterns + +### Centralized Error Handler + +```typescript +class ApiError extends Error { + constructor( + public statusCode: number, + public message: string, + public isOperational = true + ) { + super(message) + Object.setPrototypeOf(this, ApiError.prototype) + } +} + +export function errorHandler(error: unknown, req: Request): Response { + if (error instanceof ApiError) { + return NextResponse.json({ + success: false, + error: error.message + }, { status: error.statusCode }) + } + + if (error instanceof z.ZodError) { + return NextResponse.json({ + success: false, + error: 'Validation failed', + details: error.errors + }, { status: 400 }) + } + + // Log unexpected errors + console.error('Unexpected error:', error) + + return NextResponse.json({ + success: false, + error: 'Internal server error' + }, { status: 500 }) +} + +// Usage +export async function GET(request: Request) { + try { + const data = await fetchData() + return NextResponse.json({ success: true, data }) + } catch (error) { + return errorHandler(error, request) + } +} +``` + +### Retry with Exponential Backoff + +```typescript +async function fetchWithRetry( + fn: () => Promise, + maxRetries = 3 +): Promise { + let lastError: Error + + for (let i = 0; i < maxRetries; i++) { + try { + return await fn() + } catch (error) { + lastError = error as Error + + if (i < maxRetries - 1) { + // Exponential backoff: 1s, 2s, 4s + const delay = Math.pow(2, i) * 1000 + await new Promise(resolve => setTimeout(resolve, delay)) + } + } + } + + throw lastError! +} + +// Usage +const data = await fetchWithRetry(() => fetchFromAPI()) +``` + +## Authentication & Authorization + +### JWT Token Validation + +```typescript +import jwt from 'jsonwebtoken' + +interface JWTPayload { + userId: string + email: string + role: 'admin' | 'user' +} + +export function verifyToken(token: string): JWTPayload { + try { + const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload + return payload + } catch (error) { + throw new ApiError(401, 'Invalid token') + } +} + +export async function requireAuth(request: Request) { + const token = request.headers.get('authorization')?.replace('Bearer ', '') + + if (!token) { + throw new ApiError(401, 'Missing authorization token') + } + + return verifyToken(token) +} + +// Usage in API route +export async function GET(request: Request) { + const user = await requireAuth(request) + + const data = await getDataForUser(user.userId) + + return NextResponse.json({ success: true, data }) +} +``` + +### Role-Based Access Control + +```typescript +type Permission = 'read' | 'write' | 'delete' | 'admin' + +interface User { + id: string + role: 'admin' | 'moderator' | 'user' +} + +const rolePermissions: Record = { + admin: ['read', 'write', 'delete', 'admin'], + moderator: ['read', 'write', 'delete'], + user: ['read', 'write'] +} + +export function hasPermission(user: User, permission: Permission): boolean { + return rolePermissions[user.role].includes(permission) +} + +export function requirePermission(permission: Permission) { + return (handler: (request: Request, user: User) => Promise) => { + return async (request: Request) => { + const user = await requireAuth(request) + + if (!hasPermission(user, permission)) { + throw new ApiError(403, 'Insufficient permissions') + } + + return handler(request, user) + } + } +} + +// Usage - HOF wraps the handler +export const DELETE = requirePermission('delete')( + async (request: Request, user: User) => { + // Handler receives authenticated user with verified permission + return new Response('Deleted', { status: 200 }) + } +) +``` + +## Rate Limiting + +### Simple In-Memory Rate Limiter + +```typescript +class RateLimiter { + private requests = new Map() + + async checkLimit( + identifier: string, + maxRequests: number, + windowMs: number + ): Promise { + const now = Date.now() + const requests = this.requests.get(identifier) || [] + + // Remove old requests outside window + const recentRequests = requests.filter(time => now - time < windowMs) + + if (recentRequests.length >= maxRequests) { + return false // Rate limit exceeded + } + + // Add current request + recentRequests.push(now) + this.requests.set(identifier, recentRequests) + + return true + } +} + +const limiter = new RateLimiter() + +export async function GET(request: Request) { + const ip = request.headers.get('x-forwarded-for') || 'unknown' + + const allowed = await limiter.checkLimit(ip, 100, 60000) // 100 req/min + + if (!allowed) { + return NextResponse.json({ + error: 'Rate limit exceeded' + }, { status: 429 }) + } + + // Continue with request +} +``` + +## Background Jobs & Queues + +### Simple Queue Pattern + +```typescript +class JobQueue { + private queue: T[] = [] + private processing = false + + async add(job: T): Promise { + this.queue.push(job) + + if (!this.processing) { + this.process() + } + } + + private async process(): Promise { + this.processing = true + + while (this.queue.length > 0) { + const job = this.queue.shift()! + + try { + await this.execute(job) + } catch (error) { + console.error('Job failed:', error) + } + } + + this.processing = false + } + + private async execute(job: T): Promise { + // Job execution logic + } +} + +// Usage for indexing markets +interface IndexJob { + marketId: string +} + +const indexQueue = new JobQueue() + +export async function POST(request: Request) { + const { marketId } = await request.json() + + // Add to queue instead of blocking + await indexQueue.add({ marketId }) + + return NextResponse.json({ success: true, message: 'Job queued' }) +} +``` + +## Logging & Monitoring + +### Structured Logging + +```typescript +interface LogContext { + userId?: string + requestId?: string + method?: string + path?: string + [key: string]: unknown +} + +class Logger { + log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) { + const entry = { + timestamp: new Date().toISOString(), + level, + message, + ...context + } + + console.log(JSON.stringify(entry)) + } + + info(message: string, context?: LogContext) { + this.log('info', message, context) + } + + warn(message: string, context?: LogContext) { + this.log('warn', message, context) + } + + error(message: string, error: Error, context?: LogContext) { + this.log('error', message, { + ...context, + error: error.message, + stack: error.stack + }) + } +} + +const logger = new Logger() + +// Usage +export async function GET(request: Request) { + const requestId = crypto.randomUUID() + + logger.info('Fetching markets', { + requestId, + method: 'GET', + path: '/api/markets' + }) + + try { + const markets = await fetchMarkets() + return NextResponse.json({ success: true, data: markets }) + } catch (error) { + logger.error('Failed to fetch markets', error as Error, { requestId }) + return NextResponse.json({ error: 'Internal error' }, { status: 500 }) + } +} +``` + +**Remember**: Backend patterns enable scalable, maintainable server-side applications. Choose patterns that fit your complexity level. diff --git a/.agents/skills/hermes-code-review/README.md b/.agents/skills/hermes-code-review/README.md new file mode 100644 index 0000000..2ec9bee --- /dev/null +++ b/.agents/skills/hermes-code-review/README.md @@ -0,0 +1,79 @@ +# Code Review Checklist + +Systematic code review patterns covering security, performance, maintainability, correctness, and testing — with severity levels, structured feedback guidance, review process, and anti-patterns to avoid. + +## What's Inside + +- Review dimensions with priority ranking (Security → Performance → Correctness → Maintainability → Testing → Accessibility → Documentation) +- Security checklist (SQL injection, XSS, CSRF, auth, secrets, rate limiting) +- Performance checklist (N+1 queries, re-renders, memory leaks, bundle size, caching) +- Correctness checklist (edge cases, null handling, race conditions, timezone handling) +- Maintainability checklist (naming, SRP, DRY, dead code, dependency direction) +- Testing checklist (coverage, edge cases, flaky tests, mocking discipline) +- Three-pass review process (high-level → line-by-line → edge cases) +- Severity levels (Critical, Major, Minor, Nitpick) with merge-blocking guidance +- Feedback principles and example comments +- Review anti-patterns to avoid + +## When to Use + +- Reviewing pull requests or merge requests +- Establishing review standards for a team +- Improving the quality and consistency of code reviews +- Training new reviewers on what to look for + +## Installation + +```bash +npx add https://github.com/wpank/ai/tree/main/skills/testing/code-review +``` + +### OpenClaw / Moltbot / Clawbot + +```bash +npx clawhub@latest install code-review +``` + +### Manual Installation + +#### Cursor (per-project) + +From your project root: + +```bash +mkdir -p .cursor/skills +cp -r ~/.ai-skills/skills/testing/code-review .cursor/skills/code-review +``` + +#### Cursor (global) + +```bash +mkdir -p ~/.cursor/skills +cp -r ~/.ai-skills/skills/testing/code-review ~/.cursor/skills/code-review +``` + +#### Claude Code (per-project) + +From your project root: + +```bash +mkdir -p .claude/skills +cp -r ~/.ai-skills/skills/testing/code-review .claude/skills/code-review +``` + +#### Claude Code (global) + +```bash +mkdir -p ~/.claude/skills +cp -r ~/.ai-skills/skills/testing/code-review ~/.claude/skills/code-review +``` + +## Related Skills + +- [clean-code](../clean-code/) — Coding standards that reviews enforce +- [quality-gates](../quality-gates/) — Automated quality checkpoints in CI/CD +- [testing-patterns](../testing-patterns/) — Testing standards to check during review + +--- + +Part of the [Testing](..) skill category. diff --git a/.agents/skills/hermes-code-review/SKILL.md b/.agents/skills/hermes-code-review/SKILL.md new file mode 100644 index 0000000..3b0a904 --- /dev/null +++ b/.agents/skills/hermes-code-review/SKILL.md @@ -0,0 +1,232 @@ +--- +name: code-review +description: Systematic code review patterns covering security, performance, maintainability, correctness, and testing +trigger: "/hbe:review" +keywords: + - code review + - 代码审查 + - quality check + - 质量检查 + - PR review +model: reasoning +category: testing +version: 1.0 +--- + +# Code Review Checklist + +Thorough, structured approach to reviewing code. Work through each dimension systematically rather than scanning randomly. + + +## Installation + +### OpenClaw / Moltbot / Clawbot + +```bash +npx clawhub@latest install code-review +``` + + +--- + +## Review Dimensions + +| Dimension | Focus | Priority | +|-----------|-------|----------| +| Security | Vulnerabilities, auth, data exposure | Critical | +| Performance | Speed, memory, scalability bottlenecks | High | +| Correctness | Logic errors, edge cases, data integrity | High | +| Maintainability | Readability, structure, future-proofing | Medium | +| Testing | Coverage, quality, reliability of tests | Medium | +| Accessibility | WCAG compliance, keyboard nav, screen readers | Medium | +| Documentation | Comments, API docs, changelog entries | Low | + +--- + +## Security Checklist + +Review every change for these vulnerabilities: + +- [ ] **SQL Injection** — All queries use parameterized statements or an ORM; no string concatenation with user input +- [ ] **XSS** — User-provided content is escaped/sanitized before rendering; `dangerouslySetInnerHTML` or equivalent is justified and safe +- [ ] **CSRF Protection** — State-changing requests require valid CSRF tokens; SameSite cookie attributes are set +- [ ] **Authentication** — Every protected endpoint verifies the user is authenticated before processing +- [ ] **Authorization** — Resource access is scoped to the requesting user's permissions; no IDOR vulnerabilities +- [ ] **Input Validation** — All external input (params, headers, body, files) is validated for type, length, format, and range on the server side +- [ ] **Secrets Management** — No API keys, passwords, tokens, or credentials in source code; secrets come from environment variables or a vault +- [ ] **Dependency Safety** — New dependencies are from trusted sources, actively maintained, and free of known CVEs +- [ ] **Sensitive Data** — PII, tokens, and secrets are never logged, included in error messages, or returned in API responses +- [ ] **Rate Limiting** — Public and auth endpoints have rate limits to prevent brute-force and abuse +- [ ] **File Upload Safety** — Uploaded files are validated for type and size, stored outside the webroot, and served with safe Content-Type headers +- [ ] **HTTP Security Headers** — Content-Security-Policy, X-Content-Type-Options, Strict-Transport-Security are set + +--- + +## Performance Checklist + +- [ ] **N+1 Queries** — Database access patterns are batched or joined; no loops issuing individual queries +- [ ] **Unnecessary Re-renders** — Components only re-render when their relevant state/props change; memoization is applied where measurable +- [ ] **Memory Leaks** — Event listeners, subscriptions, timers, and intervals are cleaned up on unmount/disposal +- [ ] **Bundle Size** — New dependencies are tree-shakeable; large libraries are loaded dynamically; no full-library imports for a single function +- [ ] **Lazy Loading** — Heavy components, routes, and below-the-fold content use lazy loading / code splitting +- [ ] **Caching Strategy** — Expensive computations and API responses use appropriate caching (memoization, HTTP cache headers, Redis) +- [ ] **Database Indexing** — Queries filter/sort on indexed columns; new queries have been checked with EXPLAIN +- [ ] **Pagination** — List endpoints and queries use pagination or cursor-based fetching; no unbounded SELECT * +- [ ] **Async Operations** — Long-running tasks are offloaded to background jobs or queues rather than blocking request threads +- [ ] **Image & Asset Optimization** — Images are properly sized, use modern formats (WebP/AVIF), and leverage CDN delivery + +--- + +## Correctness Checklist + +- [ ] **Edge Cases** — Empty arrays, empty strings, zero values, negative numbers, and maximum values are handled +- [ ] **Null/Undefined Handling** — Nullable values are checked before access; optional chaining or guards prevent runtime errors +- [ ] **Off-by-One Errors** — Loop bounds, array slicing, pagination offsets, and range calculations are verified +- [ ] **Race Conditions** — Concurrent access to shared state uses locks, transactions, or atomic operations +- [ ] **Timezone Handling** — Dates are stored in UTC; display conversion happens at the presentation layer +- [ ] **Unicode & Encoding** — String operations handle multi-byte characters; text encoding is explicit (UTF-8) +- [ ] **Integer Overflow / Precision** — Arithmetic on large numbers or currency uses appropriate types (BigInt, Decimal) +- [ ] **Error Propagation** — Errors from async calls and external services are caught and handled; promises are never silently swallowed +- [ ] **State Consistency** — Multi-step mutations are transactional; partial failures leave the system in a valid state +- [ ] **Boundary Validation** — Values at the boundaries of valid ranges (min, max, exactly-at-limit) are tested + +--- + +## Maintainability Checklist + +- [ ] **Naming Clarity** — Variables, functions, and classes have descriptive names that reveal intent +- [ ] **Single Responsibility** — Each function/class/module does one thing; changes to one concern don't ripple through unrelated code +- [ ] **DRY** — Duplicated logic is extracted into shared utilities; copy-pasted blocks are consolidated +- [ ] **Cyclomatic Complexity** — Functions have low branching complexity; deeply nested chains are refactored +- [ ] **Error Handling** — Errors are caught at appropriate boundaries, logged with context, and surfaced meaningfully +- [ ] **Dead Code Removal** — Commented-out code, unused imports, unreachable branches, and obsolete feature flags are removed +- [ ] **Magic Numbers & Strings** — Literal values are extracted into named constants with clear semantics +- [ ] **Consistent Patterns** — New code follows the conventions already established in the codebase +- [ ] **Function Length** — Functions are short enough to understand at a glance; long functions are decomposed +- [ ] **Dependency Direction** — Dependencies point inward (infrastructure to domain); core logic does not import from UI or framework layers + +--- + +## Testing Checklist + +- [ ] **Test Coverage** — New logic paths have corresponding tests; critical paths have both happy-path and failure-case tests +- [ ] **Edge Case Tests** — Tests cover boundary values, empty inputs, nulls, and error conditions +- [ ] **No Flaky Tests** — Tests are deterministic; no reliance on timing, external services, or shared mutable state +- [ ] **Test Independence** — Each test sets up its own state and tears it down; test order does not affect results +- [ ] **Meaningful Assertions** — Tests assert on behavior and outcomes, not implementation details +- [ ] **Test Readability** — Tests follow Arrange-Act-Assert; test names describe the scenario and expected outcome +- [ ] **Mocking Discipline** — Only external boundaries (network, DB, filesystem) are mocked +- [ ] **Regression Tests** — Bug fixes include a test that reproduces the original bug and proves it is resolved + +--- + +## Review Process + +Work through the code in three passes. Do not try to catch everything in one read. + +| Pass | Focus | Time | What to Look For | +|------|-------|------|------------------| +| First | High-level structure | 2-5 min | Architecture fit, file organization, API design, overall approach | +| Second | Line-by-line detail | Bulk | Logic errors, security issues, performance problems, edge cases | +| Third | Edge cases & hardening | 5 min | Failure modes, concurrency, boundary values, missing tests | + +### First Pass (2-5 minutes) + +1. Read the PR description and linked issue +2. Scan the file list — does the change scope make sense? +3. Check the overall approach — is this the right solution to the problem? +4. Verify the change does not introduce architectural drift + +### Second Pass (bulk of review time) + +1. Read each file diff top to bottom +2. Check every function change against the checklists above +3. Verify error handling at every I/O boundary +4. Flag anything that makes you pause — trust your instincts + +### Third Pass (5 minutes) + +1. Think about what could go wrong in production +2. Check for missing tests on the code paths you flagged +3. Verify rollback safety — can this change be reverted without data loss? +4. Confirm documentation and changelog are updated if needed + +--- + +## Severity Levels + +Classify every comment by severity so the author knows what blocks merge. + +| Level | Label | Meaning | Blocks Merge? | +|-------|-------|---------|---------------| +| Critical | `[CRITICAL]` | Security vulnerability, data loss, or crash in production | Yes | +| Major | `[MAJOR]` | Bug, logic error, or significant performance regression | Yes | +| Minor | `[MINOR]` | Improvement that would reduce future maintenance cost | No | +| Nitpick | `[NIT]` | Style preference, naming suggestion, or trivial cleanup | No | + +Always prefix your review comment with the severity label. This removes ambiguity about what matters. + +--- + +## Giving Feedback + +### Principles + +- **Be specific** — Point to the exact line and explain the issue, not just "this is wrong" +- **Explain why** — State the risk or consequence, not just the rule +- **Suggest a fix** — Offer a concrete alternative or code snippet when possible +- **Ask, don't demand** — Use questions for subjective points: "What do you think about...?" +- **Acknowledge good work** — Call out clean solutions, clever optimizations, or thorough tests +- **Separate blocking from non-blocking** — Use severity labels so the author knows what matters + +### Example Comments + +**Bad:** +> This is wrong. Fix it. + +**Good:** +> `[MAJOR]` This query interpolates user input directly into the SQL string (line 42), which is vulnerable to SQL injection. Consider using a parameterized query: +> ```sql +> SELECT * FROM users WHERE id = $1 +> ``` + +**Bad:** +> Why didn't you add tests? + +**Good:** +> `[MINOR]` The new `calculateDiscount()` function has a few branching paths — could we add tests for the zero-quantity and negative-price edge cases to prevent regressions? + +**Bad:** +> I would have done this differently. + +**Good:** +> `[NIT]` This works well. An alternative approach could be extracting the retry logic into a shared `withRetry()` wrapper — but that's optional and could be a follow-up. + +--- + +## Review Anti-Patterns + +Avoid these common traps that waste time and damage team trust: + +| Anti-Pattern | Description | +|--------------|-------------| +| **Rubber-Stamping** | Approving without reading. Creates false confidence and lets bugs through. | +| **Bikeshedding** | Spending 30 minutes debating a variable name while ignoring a race condition. | +| **Blocking on Style** | Refusing to approve over formatting that a linter should enforce automatically. | +| **Gatekeeping** | Requiring your personal preferred approach when the submitted one is correct. | +| **Drive-by Reviews** | Leaving one vague comment and disappearing. Commit to following through. | +| **Scope Creep Reviews** | Requesting unrelated refactors that should be separate PRs. | +| **Stale Reviews** | Letting PRs sit for days. Review within 24 hours or hand off to someone else. | +| **Emotional Language** | "This is terrible" or "obviously wrong." Critique the code, not the person. | + +--- + +## NEVER Do + +1. **NEVER approve without reading every changed line** — rubber-stamping is worse than no review +2. **NEVER block a PR solely for style preferences** — use a linter; humans review logic +3. **NEVER leave feedback without a severity level** — ambiguity causes wasted cycles +4. **NEVER request changes without explaining why** — "fix this" teaches nothing +5. **NEVER review more than 400 lines in one sitting** — comprehension drops sharply; break large PRs into sessions +6. **NEVER skip the security checklist** — one missed vulnerability outweighs a hundred style nits +7. **NEVER make it personal** — review the code, never the coder; assume good intent diff --git a/.agents/skills/hermes-code-review/_meta.json b/.agents/skills/hermes-code-review/_meta.json new file mode 100644 index 0000000..66d4e4f --- /dev/null +++ b/.agents/skills/hermes-code-review/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn77z49xfssappp65hpybb9gx180x56e", + "slug": "code-review", + "version": "1.0.0", + "publishedAt": 1770729869941 +} \ No newline at end of file diff --git a/.agents/skills/hermes-django-patterns/SKILL.md b/.agents/skills/hermes-django-patterns/SKILL.md new file mode 100644 index 0000000..7def430 --- /dev/null +++ b/.agents/skills/hermes-django-patterns/SKILL.md @@ -0,0 +1,734 @@ +--- +name: django-patterns +description: Django architecture patterns, REST API design with DRF, ORM best practices, caching, signals, middleware, and production-grade Django apps. +origin: ECC +--- + +# Django Development Patterns + +Production-grade Django architecture patterns for scalable, maintainable applications. + +## When to Activate + +- Building Django web applications +- Designing Django REST Framework APIs +- Working with Django ORM and models +- Setting up Django project structure +- Implementing caching, signals, middleware + +## Project Structure + +### Recommended Layout + +``` +myproject/ +├── config/ +│ ├── __init__.py +│ ├── settings/ +│ │ ├── __init__.py +│ │ ├── base.py # Base settings +│ │ ├── development.py # Dev settings +│ │ ├── production.py # Production settings +│ │ └── test.py # Test settings +│ ├── urls.py +│ ├── wsgi.py +│ └── asgi.py +├── manage.py +└── apps/ + ├── __init__.py + ├── users/ + │ ├── __init__.py + │ ├── models.py + │ ├── views.py + │ ├── serializers.py + │ ├── urls.py + │ ├── permissions.py + │ ├── filters.py + │ ├── services.py + │ └── tests/ + └── products/ + └── ... +``` + +### Split Settings Pattern + +```python +# config/settings/base.py +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent.parent + +SECRET_KEY = env('DJANGO_SECRET_KEY') +DEBUG = False +ALLOWED_HOSTS = [] + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework.authtoken', + 'corsheaders', + # Local apps + 'apps.users', + 'apps.products', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'config.urls' +WSGI_APPLICATION = 'config.wsgi.application' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': env('DB_NAME'), + 'USER': env('DB_USER'), + 'PASSWORD': env('DB_PASSWORD'), + 'HOST': env('DB_HOST'), + 'PORT': env('DB_PORT', default='5432'), + } +} + +# config/settings/development.py +from .base import * + +DEBUG = True +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] + +DATABASES['default']['NAME'] = 'myproject_dev' + +INSTALLED_APPS += ['debug_toolbar'] + +MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + +# config/settings/production.py +from .base import * + +DEBUG = False +ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') +SECURE_SSL_REDIRECT = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SECURE_HSTS_SECONDS = 31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True + +# Logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'file': { + 'level': 'WARNING', + 'class': 'logging.FileHandler', + 'filename': '/var/log/django/django.log', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['file'], + 'level': 'WARNING', + 'propagate': True, + }, + }, +} +``` + +## Model Design Patterns + +### Model Best Practices + +```python +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.core.validators import MinValueValidator, MaxValueValidator + +class User(AbstractUser): + """Custom user model extending AbstractUser.""" + email = models.EmailField(unique=True) + phone = models.CharField(max_length=20, blank=True) + birth_date = models.DateField(null=True, blank=True) + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username'] + + class Meta: + db_table = 'users' + verbose_name = 'user' + verbose_name_plural = 'users' + ordering = ['-date_joined'] + + def __str__(self): + return self.email + + def get_full_name(self): + return f"{self.first_name} {self.last_name}".strip() + +class Product(models.Model): + """Product model with proper field configuration.""" + name = models.CharField(max_length=200) + slug = models.SlugField(unique=True, max_length=250) + description = models.TextField(blank=True) + price = models.DecimalField( + max_digits=10, + decimal_places=2, + validators=[MinValueValidator(0)] + ) + stock = models.PositiveIntegerField(default=0) + is_active = models.BooleanField(default=True) + category = models.ForeignKey( + 'Category', + on_delete=models.CASCADE, + related_name='products' + ) + tags = models.ManyToManyField('Tag', blank=True, related_name='products') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'products' + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['slug']), + models.Index(fields=['-created_at']), + models.Index(fields=['category', 'is_active']), + ] + constraints = [ + models.CheckConstraint( + check=models.Q(price__gte=0), + name='price_non_negative' + ) + ] + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) +``` + +### QuerySet Best Practices + +```python +from django.db import models + +class ProductQuerySet(models.QuerySet): + """Custom QuerySet for Product model.""" + + def active(self): + """Return only active products.""" + return self.filter(is_active=True) + + def with_category(self): + """Select related category to avoid N+1 queries.""" + return self.select_related('category') + + def with_tags(self): + """Prefetch tags for many-to-many relationship.""" + return self.prefetch_related('tags') + + def in_stock(self): + """Return products with stock > 0.""" + return self.filter(stock__gt=0) + + def search(self, query): + """Search products by name or description.""" + return self.filter( + models.Q(name__icontains=query) | + models.Q(description__icontains=query) + ) + +class Product(models.Model): + # ... fields ... + + objects = ProductQuerySet.as_manager() # Use custom QuerySet + +# Usage +Product.objects.active().with_category().in_stock() +``` + +### Manager Methods + +```python +class ProductManager(models.Manager): + """Custom manager for complex queries.""" + + def get_or_none(self, **kwargs): + """Return object or None instead of DoesNotExist.""" + try: + return self.get(**kwargs) + except self.model.DoesNotExist: + return None + + def create_with_tags(self, name, price, tag_names): + """Create product with associated tags.""" + product = self.create(name=name, price=price) + tags = [Tag.objects.get_or_create(name=name)[0] for name in tag_names] + product.tags.set(tags) + return product + + def bulk_update_stock(self, product_ids, quantity): + """Bulk update stock for multiple products.""" + return self.filter(id__in=product_ids).update(stock=quantity) + +# In model +class Product(models.Model): + # ... fields ... + custom = ProductManager() +``` + +## Django REST Framework Patterns + +### Serializer Patterns + +```python +from rest_framework import serializers +from django.contrib.auth.password_validation import validate_password +from .models import Product, User + +class ProductSerializer(serializers.ModelSerializer): + """Serializer for Product model.""" + + category_name = serializers.CharField(source='category.name', read_only=True) + average_rating = serializers.FloatField(read_only=True) + discount_price = serializers.SerializerMethodField() + + class Meta: + model = Product + fields = [ + 'id', 'name', 'slug', 'description', 'price', + 'discount_price', 'stock', 'category_name', + 'average_rating', 'created_at' + ] + read_only_fields = ['id', 'slug', 'created_at'] + + def get_discount_price(self, obj): + """Calculate discount price if applicable.""" + if hasattr(obj, 'discount') and obj.discount: + return obj.price * (1 - obj.discount.percent / 100) + return obj.price + + def validate_price(self, value): + """Ensure price is non-negative.""" + if value < 0: + raise serializers.ValidationError("Price cannot be negative.") + return value + +class ProductCreateSerializer(serializers.ModelSerializer): + """Serializer for creating products.""" + + class Meta: + model = Product + fields = ['name', 'description', 'price', 'stock', 'category'] + + def validate(self, data): + """Custom validation for multiple fields.""" + if data['price'] > 10000 and data['stock'] > 100: + raise serializers.ValidationError( + "Cannot have high-value products with large stock." + ) + return data + +class UserRegistrationSerializer(serializers.ModelSerializer): + """Serializer for user registration.""" + + password = serializers.CharField( + write_only=True, + required=True, + validators=[validate_password], + style={'input_type': 'password'} + ) + password_confirm = serializers.CharField(write_only=True, style={'input_type': 'password'}) + + class Meta: + model = User + fields = ['email', 'username', 'password', 'password_confirm'] + + def validate(self, data): + """Validate passwords match.""" + if data['password'] != data['password_confirm']: + raise serializers.ValidationError({ + "password_confirm": "Password fields didn't match." + }) + return data + + def create(self, validated_data): + """Create user with hashed password.""" + validated_data.pop('password_confirm') + password = validated_data.pop('password') + user = User.objects.create(**validated_data) + user.set_password(password) + user.save() + return user +``` + +### ViewSet Patterns + +```python +from rest_framework import viewsets, status, filters +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated, IsAdminUser +from django_filters.rest_framework import DjangoFilterBackend +from .models import Product +from .serializers import ProductSerializer, ProductCreateSerializer +from .permissions import IsOwnerOrReadOnly +from .filters import ProductFilter +from .services import ProductService + +class ProductViewSet(viewsets.ModelViewSet): + """ViewSet for Product model.""" + + queryset = Product.objects.select_related('category').prefetch_related('tags') + permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] + filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] + filterset_class = ProductFilter + search_fields = ['name', 'description'] + ordering_fields = ['price', 'created_at', 'name'] + ordering = ['-created_at'] + + def get_serializer_class(self): + """Return appropriate serializer based on action.""" + if self.action == 'create': + return ProductCreateSerializer + return ProductSerializer + + def perform_create(self, serializer): + """Save with user context.""" + serializer.save(created_by=self.request.user) + + @action(detail=False, methods=['get']) + def featured(self, request): + """Return featured products.""" + featured = self.queryset.filter(is_featured=True)[:10] + serializer = self.get_serializer(featured, many=True) + return Response(serializer.data) + + @action(detail=True, methods=['post']) + def purchase(self, request, pk=None): + """Purchase a product.""" + product = self.get_object() + service = ProductService() + result = service.purchase(product, request.user) + return Response(result, status=status.HTTP_201_CREATED) + + @action(detail=False, methods=['get'], permission_classes=[IsAuthenticated]) + def my_products(self, request): + """Return products created by current user.""" + products = self.queryset.filter(created_by=request.user) + page = self.paginate_queryset(products) + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) +``` + +### Custom Actions + +```python +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def add_to_cart(request): + """Add product to user cart.""" + product_id = request.data.get('product_id') + quantity = request.data.get('quantity', 1) + + try: + product = Product.objects.get(id=product_id) + except Product.DoesNotExist: + return Response( + {'error': 'Product not found'}, + status=status.HTTP_404_NOT_FOUND + ) + + cart, _ = Cart.objects.get_or_create(user=request.user) + CartItem.objects.create( + cart=cart, + product=product, + quantity=quantity + ) + + return Response({'message': 'Added to cart'}, status=status.HTTP_201_CREATED) +``` + +## Service Layer Pattern + +```python +# apps/orders/services.py +from typing import Optional +from django.db import transaction +from .models import Order, OrderItem + +class OrderService: + """Service layer for order-related business logic.""" + + @staticmethod + @transaction.atomic + def create_order(user, cart: Cart) -> Order: + """Create order from cart.""" + order = Order.objects.create( + user=user, + total_price=cart.total_price + ) + + for item in cart.items.all(): + OrderItem.objects.create( + order=order, + product=item.product, + quantity=item.quantity, + price=item.product.price + ) + + # Clear cart + cart.items.all().delete() + + return order + + @staticmethod + def process_payment(order: Order, payment_data: dict) -> bool: + """Process payment for order.""" + # Integration with payment gateway + payment = PaymentGateway.charge( + amount=order.total_price, + token=payment_data['token'] + ) + + if payment.success: + order.status = Order.Status.PAID + order.save() + # Send confirmation email + OrderService.send_confirmation_email(order) + return True + + return False + + @staticmethod + def send_confirmation_email(order: Order): + """Send order confirmation email.""" + # Email sending logic + pass +``` + +## Caching Strategies + +### View-Level Caching + +```python +from django.views.decorators.cache import cache_page +from django.utils.decorators import method_decorator + +@method_decorator(cache_page(60 * 15), name='dispatch') # 15 minutes +class ProductListView(generic.ListView): + model = Product + template_name = 'products/list.html' + context_object_name = 'products' +``` + +### Template Fragment Caching + +```django +{% load cache %} +{% cache 500 sidebar %} + ... expensive sidebar content ... +{% endcache %} +``` + +### Low-Level Caching + +```python +from django.core.cache import cache + +def get_featured_products(): + """Get featured products with caching.""" + cache_key = 'featured_products' + products = cache.get(cache_key) + + if products is None: + products = list(Product.objects.filter(is_featured=True)) + cache.set(cache_key, products, timeout=60 * 15) # 15 minutes + + return products +``` + +### QuerySet Caching + +```python +from django.core.cache import cache + +def get_popular_categories(): + cache_key = 'popular_categories' + categories = cache.get(cache_key) + + if categories is None: + categories = list(Category.objects.annotate( + product_count=Count('products') + ).filter(product_count__gt=10).order_by('-product_count')[:20]) + cache.set(cache_key, categories, timeout=60 * 60) # 1 hour + + return categories +``` + +## Signals + +### Signal Patterns + +```python +# apps/users/signals.py +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.contrib.auth import get_user_model +from .models import Profile + +User = get_user_model() + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + """Create profile when user is created.""" + if created: + Profile.objects.create(user=instance) + +@receiver(post_save, sender=User) +def save_user_profile(sender, instance, **kwargs): + """Save profile when user is saved.""" + instance.profile.save() + +# apps/users/apps.py +from django.apps import AppConfig + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.users' + + def ready(self): + """Import signals when app is ready.""" + import apps.users.signals +``` + +## Middleware + +### Custom Middleware + +```python +# middleware/active_user_middleware.py +import time +from django.utils.deprecation import MiddlewareMixin + +class ActiveUserMiddleware(MiddlewareMixin): + """Middleware to track active users.""" + + def process_request(self, request): + """Process incoming request.""" + if request.user.is_authenticated: + # Update last active time + request.user.last_active = timezone.now() + request.user.save(update_fields=['last_active']) + +class RequestLoggingMiddleware(MiddlewareMixin): + """Middleware for logging requests.""" + + def process_request(self, request): + """Log request start time.""" + request.start_time = time.time() + + def process_response(self, request, response): + """Log request duration.""" + if hasattr(request, 'start_time'): + duration = time.time() - request.start_time + logger.info(f'{request.method} {request.path} - {response.status_code} - {duration:.3f}s') + return response +``` + +## Performance Optimization + +### N+1 Query Prevention + +```python +# Bad - N+1 queries +products = Product.objects.all() +for product in products: + print(product.category.name) # Separate query for each product + +# Good - Single query with select_related +products = Product.objects.select_related('category').all() +for product in products: + print(product.category.name) + +# Good - Prefetch for many-to-many +products = Product.objects.prefetch_related('tags').all() +for product in products: + for tag in product.tags.all(): + print(tag.name) +``` + +### Database Indexing + +```python +class Product(models.Model): + name = models.CharField(max_length=200, db_index=True) + slug = models.SlugField(unique=True) + category = models.ForeignKey('Category', on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + indexes = [ + models.Index(fields=['name']), + models.Index(fields=['-created_at']), + models.Index(fields=['category', 'created_at']), + ] +``` + +### Bulk Operations + +```python +# Bulk create +Product.objects.bulk_create([ + Product(name=f'Product {i}', price=10.00) + for i in range(1000) +]) + +# Bulk update +products = Product.objects.all()[:100] +for product in products: + product.is_active = True +Product.objects.bulk_update(products, ['is_active']) + +# Bulk delete +Product.objects.filter(stock=0).delete() +``` + +## Quick Reference + +| Pattern | Description | +|---------|-------------| +| Split settings | Separate dev/prod/test settings | +| Custom QuerySet | Reusable query methods | +| Service Layer | Business logic separation | +| ViewSet | REST API endpoints | +| Serializer validation | Request/response transformation | +| select_related | Foreign key optimization | +| prefetch_related | Many-to-many optimization | +| Cache first | Cache expensive operations | +| Signals | Event-driven actions | +| Middleware | Request/response processing | + +Remember: Django provides many shortcuts, but for production applications, structure and organization matter more than concise code. Build for maintainability. diff --git a/.agents/skills/hermes-frontend-patterns/SKILL.md b/.agents/skills/hermes-frontend-patterns/SKILL.md new file mode 100644 index 0000000..ef0de63 --- /dev/null +++ b/.agents/skills/hermes-frontend-patterns/SKILL.md @@ -0,0 +1,642 @@ +--- +name: frontend-patterns +description: Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices. +origin: ECC +--- + +# Frontend Development Patterns + +Modern frontend patterns for React, Next.js, and performant user interfaces. + +## When to Activate + +- Building React components (composition, props, rendering) +- Managing state (useState, useReducer, Zustand, Context) +- Implementing data fetching (SWR, React Query, server components) +- Optimizing performance (memoization, virtualization, code splitting) +- Working with forms (validation, controlled inputs, Zod schemas) +- Handling client-side routing and navigation +- Building accessible, responsive UI patterns + +## Component Patterns + +### Composition Over Inheritance + +```typescript +// PASS: GOOD: Component composition +interface CardProps { + children: React.ReactNode + variant?: 'default' | 'outlined' +} + +export function Card({ children, variant = 'default' }: CardProps) { + return
{children}
+} + +export function CardHeader({ children }: { children: React.ReactNode }) { + return
{children}
+} + +export function CardBody({ children }: { children: React.ReactNode }) { + return
{children}
+} + +// Usage + + Title + Content + +``` + +### Compound Components + +```typescript +interface TabsContextValue { + activeTab: string + setActiveTab: (tab: string) => void +} + +const TabsContext = createContext(undefined) + +export function Tabs({ children, defaultTab }: { + children: React.ReactNode + defaultTab: string +}) { + const [activeTab, setActiveTab] = useState(defaultTab) + + return ( + + {children} + + ) +} + +export function TabList({ children }: { children: React.ReactNode }) { + return
{children}
+} + +export function Tab({ id, children }: { id: string, children: React.ReactNode }) { + const context = useContext(TabsContext) + if (!context) throw new Error('Tab must be used within Tabs') + + return ( + + ) +} + +// Usage + + + Overview + Details + + +``` + +### Render Props Pattern + +```typescript +interface DataLoaderProps { + url: string + children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode +} + +export function DataLoader({ url, children }: DataLoaderProps) { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + fetch(url) + .then(res => res.json()) + .then(setData) + .catch(setError) + .finally(() => setLoading(false)) + }, [url]) + + return <>{children(data, loading, error)} +} + +// Usage + url="/api/markets"> + {(markets, loading, error) => { + if (loading) return + if (error) return + return + }} + +``` + +## Custom Hooks Patterns + +### State Management Hook + +```typescript +export function useToggle(initialValue = false): [boolean, () => void] { + const [value, setValue] = useState(initialValue) + + const toggle = useCallback(() => { + setValue(v => !v) + }, []) + + return [value, toggle] +} + +// Usage +const [isOpen, toggleOpen] = useToggle() +``` + +### Async Data Fetching Hook + +```typescript +interface UseQueryOptions { + onSuccess?: (data: T) => void + onError?: (error: Error) => void + enabled?: boolean +} + +export function useQuery( + key: string, + fetcher: () => Promise, + options?: UseQueryOptions +) { + const [data, setData] = useState(null) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + const refetch = useCallback(async () => { + setLoading(true) + setError(null) + + try { + const result = await fetcher() + setData(result) + options?.onSuccess?.(result) + } catch (err) { + const error = err as Error + setError(error) + options?.onError?.(error) + } finally { + setLoading(false) + } + }, [fetcher, options]) + + useEffect(() => { + if (options?.enabled !== false) { + refetch() + } + }, [key, refetch, options?.enabled]) + + return { data, error, loading, refetch } +} + +// Usage +const { data: markets, loading, error, refetch } = useQuery( + 'markets', + () => fetch('/api/markets').then(r => r.json()), + { + onSuccess: data => console.log('Fetched', data.length, 'markets'), + onError: err => console.error('Failed:', err) + } +) +``` + +### Debounce Hook + +```typescript +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => clearTimeout(handler) + }, [value, delay]) + + return debouncedValue +} + +// Usage +const [searchQuery, setSearchQuery] = useState('') +const debouncedQuery = useDebounce(searchQuery, 500) + +useEffect(() => { + if (debouncedQuery) { + performSearch(debouncedQuery) + } +}, [debouncedQuery]) +``` + +## State Management Patterns + +### Context + Reducer Pattern + +```typescript +interface State { + markets: Market[] + selectedMarket: Market | null + loading: boolean +} + +type Action = + | { type: 'SET_MARKETS'; payload: Market[] } + | { type: 'SELECT_MARKET'; payload: Market } + | { type: 'SET_LOADING'; payload: boolean } + +function reducer(state: State, action: Action): State { + switch (action.type) { + case 'SET_MARKETS': + return { ...state, markets: action.payload } + case 'SELECT_MARKET': + return { ...state, selectedMarket: action.payload } + case 'SET_LOADING': + return { ...state, loading: action.payload } + default: + return state + } +} + +const MarketContext = createContext<{ + state: State + dispatch: Dispatch +} | undefined>(undefined) + +export function MarketProvider({ children }: { children: React.ReactNode }) { + const [state, dispatch] = useReducer(reducer, { + markets: [], + selectedMarket: null, + loading: false + }) + + return ( + + {children} + + ) +} + +export function useMarkets() { + const context = useContext(MarketContext) + if (!context) throw new Error('useMarkets must be used within MarketProvider') + return context +} +``` + +## Performance Optimization + +### Memoization + +```typescript +// PASS: useMemo for expensive computations +const sortedMarkets = useMemo(() => { + return markets.sort((a, b) => b.volume - a.volume) +}, [markets]) + +// PASS: useCallback for functions passed to children +const handleSearch = useCallback((query: string) => { + setSearchQuery(query) +}, []) + +// PASS: React.memo for pure components +export const MarketCard = React.memo(({ market }) => { + return ( +
+

{market.name}

+

{market.description}

+
+ ) +}) +``` + +### Code Splitting & Lazy Loading + +```typescript +import { lazy, Suspense } from 'react' + +// PASS: Lazy load heavy components +const HeavyChart = lazy(() => import('./HeavyChart')) +const ThreeJsBackground = lazy(() => import('./ThreeJsBackground')) + +export function Dashboard() { + return ( +
+ }> + + + + + + +
+ ) +} +``` + +### Virtualization for Long Lists + +```typescript +import { useVirtualizer } from '@tanstack/react-virtual' + +export function VirtualMarketList({ markets }: { markets: Market[] }) { + const parentRef = useRef(null) + + const virtualizer = useVirtualizer({ + count: markets.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 100, // Estimated row height + overscan: 5 // Extra items to render + }) + + return ( +
+
+ {virtualizer.getVirtualItems().map(virtualRow => ( +
+ +
+ ))} +
+
+ ) +} +``` + +## Form Handling Patterns + +### Controlled Form with Validation + +```typescript +interface FormData { + name: string + description: string + endDate: string +} + +interface FormErrors { + name?: string + description?: string + endDate?: string +} + +export function CreateMarketForm() { + const [formData, setFormData] = useState({ + name: '', + description: '', + endDate: '' + }) + + const [errors, setErrors] = useState({}) + + const validate = (): boolean => { + const newErrors: FormErrors = {} + + if (!formData.name.trim()) { + newErrors.name = 'Name is required' + } else if (formData.name.length > 200) { + newErrors.name = 'Name must be under 200 characters' + } + + if (!formData.description.trim()) { + newErrors.description = 'Description is required' + } + + if (!formData.endDate) { + newErrors.endDate = 'End date is required' + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!validate()) return + + try { + await createMarket(formData) + // Success handling + } catch (error) { + // Error handling + } + } + + return ( +
+ setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="Market name" + /> + {errors.name && {errors.name}} + + {/* Other fields */} + + +
+ ) +} +``` + +## Error Boundary Pattern + +```typescript +interface ErrorBoundaryState { + hasError: boolean + error: Error | null +} + +export class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + ErrorBoundaryState +> { + state: ErrorBoundaryState = { + hasError: false, + error: null + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('Error boundary caught:', error, errorInfo) + } + + render() { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

{this.state.error?.message}

+ +
+ ) + } + + return this.props.children + } +} + +// Usage + + + +``` + +## Animation Patterns + +### Framer Motion Animations + +```typescript +import { motion, AnimatePresence } from 'framer-motion' + +// PASS: List animations +export function AnimatedMarketList({ markets }: { markets: Market[] }) { + return ( + + {markets.map(market => ( + + + + ))} + + ) +} + +// PASS: Modal animations +export function Modal({ isOpen, onClose, children }: ModalProps) { + return ( + + {isOpen && ( + <> + + + {children} + + + )} + + ) +} +``` + +## Accessibility Patterns + +### Keyboard Navigation + +```typescript +export function Dropdown({ options, onSelect }: DropdownProps) { + const [isOpen, setIsOpen] = useState(false) + const [activeIndex, setActiveIndex] = useState(0) + + const handleKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + setActiveIndex(i => Math.min(i + 1, options.length - 1)) + break + case 'ArrowUp': + e.preventDefault() + setActiveIndex(i => Math.max(i - 1, 0)) + break + case 'Enter': + e.preventDefault() + onSelect(options[activeIndex]) + setIsOpen(false) + break + case 'Escape': + setIsOpen(false) + break + } + } + + return ( +
+ {/* Dropdown implementation */} +
+ ) +} +``` + +### Focus Management + +```typescript +export function Modal({ isOpen, onClose, children }: ModalProps) { + const modalRef = useRef(null) + const previousFocusRef = useRef(null) + + useEffect(() => { + if (isOpen) { + // Save currently focused element + previousFocusRef.current = document.activeElement as HTMLElement + + // Focus modal + modalRef.current?.focus() + } else { + // Restore focus when closing + previousFocusRef.current?.focus() + } + }, [isOpen]) + + return isOpen ? ( +
e.key === 'Escape' && onClose()} + > + {children} +
+ ) : null +} +``` + +**Remember**: Modern frontend patterns enable maintainable, performant user interfaces. Choose patterns that fit your project complexity. diff --git a/.agents/skills/hermes-python-patterns/SKILL.md b/.agents/skills/hermes-python-patterns/SKILL.md new file mode 100644 index 0000000..ba1156d --- /dev/null +++ b/.agents/skills/hermes-python-patterns/SKILL.md @@ -0,0 +1,750 @@ +--- +name: python-patterns +description: Pythonic idioms, PEP 8 standards, type hints, and best practices for building robust, efficient, and maintainable Python applications. +origin: ECC +--- + +# Python Development Patterns + +Idiomatic Python patterns and best practices for building robust, efficient, and maintainable applications. + +## When to Activate + +- Writing new Python code +- Reviewing Python code +- Refactoring existing Python code +- Designing Python packages/modules + +## Core Principles + +### 1. Readability Counts + +Python prioritizes readability. Code should be obvious and easy to understand. + +```python +# Good: Clear and readable +def get_active_users(users: list[User]) -> list[User]: + """Return only active users from the provided list.""" + return [user for user in users if user.is_active] + + +# Bad: Clever but confusing +def get_active_users(u): + return [x for x in u if x.a] +``` + +### 2. Explicit is Better Than Implicit + +Avoid magic; be clear about what your code does. + +```python +# Good: Explicit configuration +import logging + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +# Bad: Hidden side effects +import some_module +some_module.setup() # What does this do? +``` + +### 3. EAFP - Easier to Ask Forgiveness Than Permission + +Python prefers exception handling over checking conditions. + +```python +# Good: EAFP style +def get_value(dictionary: dict, key: str) -> Any: + try: + return dictionary[key] + except KeyError: + return default_value + +# Bad: LBYL (Look Before You Leap) style +def get_value(dictionary: dict, key: str) -> Any: + if key in dictionary: + return dictionary[key] + else: + return default_value +``` + +## Type Hints + +### Basic Type Annotations + +```python +from typing import Optional, List, Dict, Any + +def process_user( + user_id: str, + data: Dict[str, Any], + active: bool = True +) -> Optional[User]: + """Process a user and return the updated User or None.""" + if not active: + return None + return User(user_id, data) +``` + +### Modern Type Hints (Python 3.9+) + +```python +# Python 3.9+ - Use built-in types +def process_items(items: list[str]) -> dict[str, int]: + return {item: len(item) for item in items} + +# Python 3.8 and earlier - Use typing module +from typing import List, Dict + +def process_items(items: List[str]) -> Dict[str, int]: + return {item: len(item) for item in items} +``` + +### Type Aliases and TypeVar + +```python +from typing import TypeVar, Union + +# Type alias for complex types +JSON = Union[dict[str, Any], list[Any], str, int, float, bool, None] + +def parse_json(data: str) -> JSON: + return json.loads(data) + +# Generic types +T = TypeVar('T') + +def first(items: list[T]) -> T | None: + """Return the first item or None if list is empty.""" + return items[0] if items else None +``` + +### Protocol-Based Duck Typing + +```python +from typing import Protocol + +class Renderable(Protocol): + def render(self) -> str: + """Render the object to a string.""" + +def render_all(items: list[Renderable]) -> str: + """Render all items that implement the Renderable protocol.""" + return "\n".join(item.render() for item in items) +``` + +## Error Handling Patterns + +### Specific Exception Handling + +```python +# Good: Catch specific exceptions +def load_config(path: str) -> Config: + try: + with open(path) as f: + return Config.from_json(f.read()) + except FileNotFoundError as e: + raise ConfigError(f"Config file not found: {path}") from e + except json.JSONDecodeError as e: + raise ConfigError(f"Invalid JSON in config: {path}") from e + +# Bad: Bare except +def load_config(path: str) -> Config: + try: + with open(path) as f: + return Config.from_json(f.read()) + except: + return None # Silent failure! +``` + +### Exception Chaining + +```python +def process_data(data: str) -> Result: + try: + parsed = json.loads(data) + except json.JSONDecodeError as e: + # Chain exceptions to preserve the traceback + raise ValueError(f"Failed to parse data: {data}") from e +``` + +### Custom Exception Hierarchy + +```python +class AppError(Exception): + """Base exception for all application errors.""" + pass + +class ValidationError(AppError): + """Raised when input validation fails.""" + pass + +class NotFoundError(AppError): + """Raised when a requested resource is not found.""" + pass + +# Usage +def get_user(user_id: str) -> User: + user = db.find_user(user_id) + if not user: + raise NotFoundError(f"User not found: {user_id}") + return user +``` + +## Context Managers + +### Resource Management + +```python +# Good: Using context managers +def process_file(path: str) -> str: + with open(path, 'r') as f: + return f.read() + +# Bad: Manual resource management +def process_file(path: str) -> str: + f = open(path, 'r') + try: + return f.read() + finally: + f.close() +``` + +### Custom Context Managers + +```python +from contextlib import contextmanager + +@contextmanager +def timer(name: str): + """Context manager to time a block of code.""" + start = time.perf_counter() + yield + elapsed = time.perf_counter() - start + print(f"{name} took {elapsed:.4f} seconds") + +# Usage +with timer("data processing"): + process_large_dataset() +``` + +### Context Manager Classes + +```python +class DatabaseTransaction: + def __init__(self, connection): + self.connection = connection + + def __enter__(self): + self.connection.begin_transaction() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is None: + self.connection.commit() + else: + self.connection.rollback() + return False # Don't suppress exceptions + +# Usage +with DatabaseTransaction(conn): + user = conn.create_user(user_data) + conn.create_profile(user.id, profile_data) +``` + +## Comprehensions and Generators + +### List Comprehensions + +```python +# Good: List comprehension for simple transformations +names = [user.name for user in users if user.is_active] + +# Bad: Manual loop +names = [] +for user in users: + if user.is_active: + names.append(user.name) + +# Complex comprehensions should be expanded +# Bad: Too complex +result = [x * 2 for x in items if x > 0 if x % 2 == 0] + +# Good: Use a generator function +def filter_and_transform(items: Iterable[int]) -> list[int]: + result = [] + for x in items: + if x > 0 and x % 2 == 0: + result.append(x * 2) + return result +``` + +### Generator Expressions + +```python +# Good: Generator for lazy evaluation +total = sum(x * x for x in range(1_000_000)) + +# Bad: Creates large intermediate list +total = sum([x * x for x in range(1_000_000)]) +``` + +### Generator Functions + +```python +def read_large_file(path: str) -> Iterator[str]: + """Read a large file line by line.""" + with open(path) as f: + for line in f: + yield line.strip() + +# Usage +for line in read_large_file("huge.txt"): + process(line) +``` + +## Data Classes and Named Tuples + +### Data Classes + +```python +from dataclasses import dataclass, field +from datetime import datetime + +@dataclass +class User: + """User entity with automatic __init__, __repr__, and __eq__.""" + id: str + name: str + email: str + created_at: datetime = field(default_factory=datetime.now) + is_active: bool = True + +# Usage +user = User( + id="123", + name="Alice", + email="alice@example.com" +) +``` + +### Data Classes with Validation + +```python +@dataclass +class User: + email: str + age: int + + def __post_init__(self): + # Validate email format + if "@" not in self.email: + raise ValueError(f"Invalid email: {self.email}") + # Validate age range + if self.age < 0 or self.age > 150: + raise ValueError(f"Invalid age: {self.age}") +``` + +### Named Tuples + +```python +from typing import NamedTuple + +class Point(NamedTuple): + """Immutable 2D point.""" + x: float + y: float + + def distance(self, other: 'Point') -> float: + return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5 + +# Usage +p1 = Point(0, 0) +p2 = Point(3, 4) +print(p1.distance(p2)) # 5.0 +``` + +## Decorators + +### Function Decorators + +```python +import functools +import time + +def timer(func: Callable) -> Callable: + """Decorator to time function execution.""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + start = time.perf_counter() + result = func(*args, **kwargs) + elapsed = time.perf_counter() - start + print(f"{func.__name__} took {elapsed:.4f}s") + return result + return wrapper + +@timer +def slow_function(): + time.sleep(1) + +# slow_function() prints: slow_function took 1.0012s +``` + +### Parameterized Decorators + +```python +def repeat(times: int): + """Decorator to repeat a function multiple times.""" + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def wrapper(*args, **kwargs): + results = [] + for _ in range(times): + results.append(func(*args, **kwargs)) + return results + return wrapper + return decorator + +@repeat(times=3) +def greet(name: str) -> str: + return f"Hello, {name}!" + +# greet("Alice") returns ["Hello, Alice!", "Hello, Alice!", "Hello, Alice!"] +``` + +### Class-Based Decorators + +```python +class CountCalls: + """Decorator that counts how many times a function is called.""" + def __init__(self, func: Callable): + functools.update_wrapper(self, func) + self.func = func + self.count = 0 + + def __call__(self, *args, **kwargs): + self.count += 1 + print(f"{self.func.__name__} has been called {self.count} times") + return self.func(*args, **kwargs) + +@CountCalls +def process(): + pass + +# Each call to process() prints the call count +``` + +## Concurrency Patterns + +### Threading for I/O-Bound Tasks + +```python +import concurrent.futures +import threading + +def fetch_url(url: str) -> str: + """Fetch a URL (I/O-bound operation).""" + import urllib.request + with urllib.request.urlopen(url) as response: + return response.read().decode() + +def fetch_all_urls(urls: list[str]) -> dict[str, str]: + """Fetch multiple URLs concurrently using threads.""" + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + future_to_url = {executor.submit(fetch_url, url): url for url in urls} + results = {} + for future in concurrent.futures.as_completed(future_to_url): + url = future_to_url[future] + try: + results[url] = future.result() + except Exception as e: + results[url] = f"Error: {e}" + return results +``` + +### Multiprocessing for CPU-Bound Tasks + +```python +def process_data(data: list[int]) -> int: + """CPU-intensive computation.""" + return sum(x ** 2 for x in data) + +def process_all(datasets: list[list[int]]) -> list[int]: + """Process multiple datasets using multiple processes.""" + with concurrent.futures.ProcessPoolExecutor() as executor: + results = list(executor.map(process_data, datasets)) + return results +``` + +### Async/Await for Concurrent I/O + +```python +import asyncio + +async def fetch_async(url: str) -> str: + """Fetch a URL asynchronously.""" + import aiohttp + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + return await response.text() + +async def fetch_all(urls: list[str]) -> dict[str, str]: + """Fetch multiple URLs concurrently.""" + tasks = [fetch_async(url) for url in urls] + results = await asyncio.gather(*tasks, return_exceptions=True) + return dict(zip(urls, results)) +``` + +## Package Organization + +### Standard Project Layout + +``` +myproject/ +├── src/ +│ └── mypackage/ +│ ├── __init__.py +│ ├── main.py +│ ├── api/ +│ │ ├── __init__.py +│ │ └── routes.py +│ ├── models/ +│ │ ├── __init__.py +│ │ └── user.py +│ └── utils/ +│ ├── __init__.py +│ └── helpers.py +├── tests/ +│ ├── __init__.py +│ ├── conftest.py +│ ├── test_api.py +│ └── test_models.py +├── pyproject.toml +├── README.md +└── .gitignore +``` + +### Import Conventions + +```python +# Good: Import order - stdlib, third-party, local +import os +import sys +from pathlib import Path + +import requests +from fastapi import FastAPI + +from mypackage.models import User +from mypackage.utils import format_name + +# Good: Use isort for automatic import sorting +# pip install isort +``` + +### __init__.py for Package Exports + +```python +# mypackage/__init__.py +"""mypackage - A sample Python package.""" + +__version__ = "1.0.0" + +# Export main classes/functions at package level +from mypackage.models import User, Post +from mypackage.utils import format_name + +__all__ = ["User", "Post", "format_name"] +``` + +## Memory and Performance + +### Using __slots__ for Memory Efficiency + +```python +# Bad: Regular class uses __dict__ (more memory) +class Point: + def __init__(self, x: float, y: float): + self.x = x + self.y = y + +# Good: __slots__ reduces memory usage +class Point: + __slots__ = ['x', 'y'] + + def __init__(self, x: float, y: float): + self.x = x + self.y = y +``` + +### Generator for Large Data + +```python +# Bad: Returns full list in memory +def read_lines(path: str) -> list[str]: + with open(path) as f: + return [line.strip() for line in f] + +# Good: Yields lines one at a time +def read_lines(path: str) -> Iterator[str]: + with open(path) as f: + for line in f: + yield line.strip() +``` + +### Avoid String Concatenation in Loops + +```python +# Bad: O(n²) due to string immutability +result = "" +for item in items: + result += str(item) + +# Good: O(n) using join +result = "".join(str(item) for item in items) + +# Good: Using StringIO for building +from io import StringIO + +buffer = StringIO() +for item in items: + buffer.write(str(item)) +result = buffer.getvalue() +``` + +## Python Tooling Integration + +### Essential Commands + +```bash +# Code formatting +black . +isort . + +# Linting +ruff check . +pylint mypackage/ + +# Type checking +mypy . + +# Testing +pytest --cov=mypackage --cov-report=html + +# Security scanning +bandit -r . + +# Dependency management +pip-audit +safety check +``` + +### pyproject.toml Configuration + +```toml +[project] +name = "mypackage" +version = "1.0.0" +requires-python = ">=3.9" +dependencies = [ + "requests>=2.31.0", + "pydantic>=2.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.4.0", + "pytest-cov>=4.1.0", + "black>=23.0.0", + "ruff>=0.1.0", + "mypy>=1.5.0", +] + +[tool.black] +line-length = 88 +target-version = ['py39'] + +[tool.ruff] +line-length = 88 +select = ["E", "F", "I", "N", "W"] + +[tool.mypy] +python_version = "3.9" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "--cov=mypackage --cov-report=term-missing" +``` + +## Quick Reference: Python Idioms + +| Idiom | Description | +|-------|-------------| +| EAFP | Easier to Ask Forgiveness than Permission | +| Context managers | Use `with` for resource management | +| List comprehensions | For simple transformations | +| Generators | For lazy evaluation and large datasets | +| Type hints | Annotate function signatures | +| Dataclasses | For data containers with auto-generated methods | +| `__slots__` | For memory optimization | +| f-strings | For string formatting (Python 3.6+) | +| `pathlib.Path` | For path operations (Python 3.4+) | +| `enumerate` | For index-element pairs in loops | + +## Anti-Patterns to Avoid + +```python +# Bad: Mutable default arguments +def append_to(item, items=[]): + items.append(item) + return items + +# Good: Use None and create new list +def append_to(item, items=None): + if items is None: + items = [] + items.append(item) + return items + +# Bad: Checking type with type() +if type(obj) == list: + process(obj) + +# Good: Use isinstance +if isinstance(obj, list): + process(obj) + +# Bad: Comparing to None with == +if value == None: + process() + +# Good: Use is +if value is None: + process() + +# Bad: from module import * +from os.path import * + +# Good: Explicit imports +from os.path import join, exists + +# Bad: Bare except +try: + risky_operation() +except: + pass + +# Good: Specific exception +try: + risky_operation() +except SpecificError as e: + logger.error(f"Operation failed: {e}") +``` + +__Remember__: Python code should be readable, explicit, and follow the principle of least surprise. When in doubt, prioritize clarity over cleverness. diff --git a/.agents/skills/hermes-python-testing/SKILL.md b/.agents/skills/hermes-python-testing/SKILL.md new file mode 100644 index 0000000..85e3661 --- /dev/null +++ b/.agents/skills/hermes-python-testing/SKILL.md @@ -0,0 +1,816 @@ +--- +name: python-testing +description: Python testing strategies using pytest, TDD methodology, fixtures, mocking, parametrization, and coverage requirements. +origin: ECC +--- + +# Python Testing Patterns + +Comprehensive testing strategies for Python applications using pytest, TDD methodology, and best practices. + +## When to Activate + +- Writing new Python code (follow TDD: red, green, refactor) +- Designing test suites for Python projects +- Reviewing Python test coverage +- Setting up testing infrastructure + +## Core Testing Philosophy + +### Test-Driven Development (TDD) + +Always follow the TDD cycle: + +1. **RED**: Write a failing test for the desired behavior +2. **GREEN**: Write minimal code to make the test pass +3. **REFACTOR**: Improve code while keeping tests green + +```python +# Step 1: Write failing test (RED) +def test_add_numbers(): + result = add(2, 3) + assert result == 5 + +# Step 2: Write minimal implementation (GREEN) +def add(a, b): + return a + b + +# Step 3: Refactor if needed (REFACTOR) +``` + +### Coverage Requirements + +- **Target**: 80%+ code coverage +- **Critical paths**: 100% coverage required +- Use `pytest --cov` to measure coverage + +```bash +pytest --cov=mypackage --cov-report=term-missing --cov-report=html +``` + +## pytest Fundamentals + +### Basic Test Structure + +```python +import pytest + +def test_addition(): + """Test basic addition.""" + assert 2 + 2 == 4 + +def test_string_uppercase(): + """Test string uppercasing.""" + text = "hello" + assert text.upper() == "HELLO" + +def test_list_append(): + """Test list append.""" + items = [1, 2, 3] + items.append(4) + assert 4 in items + assert len(items) == 4 +``` + +### Assertions + +```python +# Equality +assert result == expected + +# Inequality +assert result != unexpected + +# Truthiness +assert result # Truthy +assert not result # Falsy +assert result is True # Exactly True +assert result is False # Exactly False +assert result is None # Exactly None + +# Membership +assert item in collection +assert item not in collection + +# Comparisons +assert result > 0 +assert 0 <= result <= 100 + +# Type checking +assert isinstance(result, str) + +# Exception testing (preferred approach) +with pytest.raises(ValueError): + raise ValueError("error message") + +# Check exception message +with pytest.raises(ValueError, match="invalid input"): + raise ValueError("invalid input provided") + +# Check exception attributes +with pytest.raises(ValueError) as exc_info: + raise ValueError("error message") +assert str(exc_info.value) == "error message" +``` + +## Fixtures + +### Basic Fixture Usage + +```python +import pytest + +@pytest.fixture +def sample_data(): + """Fixture providing sample data.""" + return {"name": "Alice", "age": 30} + +def test_sample_data(sample_data): + """Test using the fixture.""" + assert sample_data["name"] == "Alice" + assert sample_data["age"] == 30 +``` + +### Fixture with Setup/Teardown + +```python +@pytest.fixture +def database(): + """Fixture with setup and teardown.""" + # Setup + db = Database(":memory:") + db.create_tables() + db.insert_test_data() + + yield db # Provide to test + + # Teardown + db.close() + +def test_database_query(database): + """Test database operations.""" + result = database.query("SELECT * FROM users") + assert len(result) > 0 +``` + +### Fixture Scopes + +```python +# Function scope (default) - runs for each test +@pytest.fixture +def temp_file(): + with open("temp.txt", "w") as f: + yield f + os.remove("temp.txt") + +# Module scope - runs once per module +@pytest.fixture(scope="module") +def module_db(): + db = Database(":memory:") + db.create_tables() + yield db + db.close() + +# Session scope - runs once per test session +@pytest.fixture(scope="session") +def shared_resource(): + resource = ExpensiveResource() + yield resource + resource.cleanup() +``` + +### Fixture with Parameters + +```python +@pytest.fixture(params=[1, 2, 3]) +def number(request): + """Parameterized fixture.""" + return request.param + +def test_numbers(number): + """Test runs 3 times, once for each parameter.""" + assert number > 0 +``` + +### Using Multiple Fixtures + +```python +@pytest.fixture +def user(): + return User(id=1, name="Alice") + +@pytest.fixture +def admin(): + return User(id=2, name="Admin", role="admin") + +def test_user_admin_interaction(user, admin): + """Test using multiple fixtures.""" + assert admin.can_manage(user) +``` + +### Autouse Fixtures + +```python +@pytest.fixture(autouse=True) +def reset_config(): + """Automatically runs before every test.""" + Config.reset() + yield + Config.cleanup() + +def test_without_fixture_call(): + # reset_config runs automatically + assert Config.get_setting("debug") is False +``` + +### Conftest.py for Shared Fixtures + +```python +# tests/conftest.py +import pytest + +@pytest.fixture +def client(): + """Shared fixture for all tests.""" + app = create_app(testing=True) + with app.test_client() as client: + yield client + +@pytest.fixture +def auth_headers(client): + """Generate auth headers for API testing.""" + response = client.post("/api/login", json={ + "username": "test", + "password": "test" + }) + token = response.json["token"] + return {"Authorization": f"Bearer {token}"} +``` + +## Parametrization + +### Basic Parametrization + +```python +@pytest.mark.parametrize("input,expected", [ + ("hello", "HELLO"), + ("world", "WORLD"), + ("PyThOn", "PYTHON"), +]) +def test_uppercase(input, expected): + """Test runs 3 times with different inputs.""" + assert input.upper() == expected +``` + +### Multiple Parameters + +```python +@pytest.mark.parametrize("a,b,expected", [ + (2, 3, 5), + (0, 0, 0), + (-1, 1, 0), + (100, 200, 300), +]) +def test_add(a, b, expected): + """Test addition with multiple inputs.""" + assert add(a, b) == expected +``` + +### Parametrize with IDs + +```python +@pytest.mark.parametrize("input,expected", [ + ("valid@email.com", True), + ("invalid", False), + ("@no-domain.com", False), +], ids=["valid-email", "missing-at", "missing-domain"]) +def test_email_validation(input, expected): + """Test email validation with readable test IDs.""" + assert is_valid_email(input) is expected +``` + +### Parametrized Fixtures + +```python +@pytest.fixture(params=["sqlite", "postgresql", "mysql"]) +def db(request): + """Test against multiple database backends.""" + if request.param == "sqlite": + return Database(":memory:") + elif request.param == "postgresql": + return Database("postgresql://localhost/test") + elif request.param == "mysql": + return Database("mysql://localhost/test") + +def test_database_operations(db): + """Test runs 3 times, once for each database.""" + result = db.query("SELECT 1") + assert result is not None +``` + +## Markers and Test Selection + +### Custom Markers + +```python +# Mark slow tests +@pytest.mark.slow +def test_slow_operation(): + time.sleep(5) + +# Mark integration tests +@pytest.mark.integration +def test_api_integration(): + response = requests.get("https://api.example.com") + assert response.status_code == 200 + +# Mark unit tests +@pytest.mark.unit +def test_unit_logic(): + assert calculate(2, 3) == 5 +``` + +### Run Specific Tests + +```bash +# Run only fast tests +pytest -m "not slow" + +# Run only integration tests +pytest -m integration + +# Run integration or slow tests +pytest -m "integration or slow" + +# Run tests marked as unit but not slow +pytest -m "unit and not slow" +``` + +### Configure Markers in pytest.ini + +```ini +[pytest] +markers = + slow: marks tests as slow + integration: marks tests as integration tests + unit: marks tests as unit tests + django: marks tests as requiring Django +``` + +## Mocking and Patching + +### Mocking Functions + +```python +from unittest.mock import patch, Mock + +@patch("mypackage.external_api_call") +def test_with_mock(api_call_mock): + """Test with mocked external API.""" + api_call_mock.return_value = {"status": "success"} + + result = my_function() + + api_call_mock.assert_called_once() + assert result["status"] == "success" +``` + +### Mocking Return Values + +```python +@patch("mypackage.Database.connect") +def test_database_connection(connect_mock): + """Test with mocked database connection.""" + connect_mock.return_value = MockConnection() + + db = Database() + db.connect() + + connect_mock.assert_called_once_with("localhost") +``` + +### Mocking Exceptions + +```python +@patch("mypackage.api_call") +def test_api_error_handling(api_call_mock): + """Test error handling with mocked exception.""" + api_call_mock.side_effect = ConnectionError("Network error") + + with pytest.raises(ConnectionError): + api_call() + + api_call_mock.assert_called_once() +``` + +### Mocking Context Managers + +```python +@patch("builtins.open", new_callable=mock_open) +def test_file_reading(mock_file): + """Test file reading with mocked open.""" + mock_file.return_value.read.return_value = "file content" + + result = read_file("test.txt") + + mock_file.assert_called_once_with("test.txt", "r") + assert result == "file content" +``` + +### Using Autospec + +```python +@patch("mypackage.DBConnection", autospec=True) +def test_autospec(db_mock): + """Test with autospec to catch API misuse.""" + db = db_mock.return_value + db.query("SELECT * FROM users") + + # This would fail if DBConnection doesn't have query method + db_mock.assert_called_once() +``` + +### Mock Class Instances + +```python +class TestUserService: + @patch("mypackage.UserRepository") + def test_create_user(self, repo_mock): + """Test user creation with mocked repository.""" + repo_mock.return_value.save.return_value = User(id=1, name="Alice") + + service = UserService(repo_mock.return_value) + user = service.create_user(name="Alice") + + assert user.name == "Alice" + repo_mock.return_value.save.assert_called_once() +``` + +### Mock Property + +```python +@pytest.fixture +def mock_config(): + """Create a mock with a property.""" + config = Mock() + type(config).debug = PropertyMock(return_value=True) + type(config).api_key = PropertyMock(return_value="test-key") + return config + +def test_with_mock_config(mock_config): + """Test with mocked config properties.""" + assert mock_config.debug is True + assert mock_config.api_key == "test-key" +``` + +## Testing Async Code + +### Async Tests with pytest-asyncio + +```python +import pytest + +@pytest.mark.asyncio +async def test_async_function(): + """Test async function.""" + result = await async_add(2, 3) + assert result == 5 + +@pytest.mark.asyncio +async def test_async_with_fixture(async_client): + """Test async with async fixture.""" + response = await async_client.get("/api/users") + assert response.status_code == 200 +``` + +### Async Fixture + +```python +@pytest.fixture +async def async_client(): + """Async fixture providing async test client.""" + app = create_app() + async with app.test_client() as client: + yield client + +@pytest.mark.asyncio +async def test_api_endpoint(async_client): + """Test using async fixture.""" + response = await async_client.get("/api/data") + assert response.status_code == 200 +``` + +### Mocking Async Functions + +```python +@pytest.mark.asyncio +@patch("mypackage.async_api_call") +async def test_async_mock(api_call_mock): + """Test async function with mock.""" + api_call_mock.return_value = {"status": "ok"} + + result = await my_async_function() + + api_call_mock.assert_awaited_once() + assert result["status"] == "ok" +``` + +## Testing Exceptions + +### Testing Expected Exceptions + +```python +def test_divide_by_zero(): + """Test that dividing by zero raises ZeroDivisionError.""" + with pytest.raises(ZeroDivisionError): + divide(10, 0) + +def test_custom_exception(): + """Test custom exception with message.""" + with pytest.raises(ValueError, match="invalid input"): + validate_input("invalid") +``` + +### Testing Exception Attributes + +```python +def test_exception_with_details(): + """Test exception with custom attributes.""" + with pytest.raises(CustomError) as exc_info: + raise CustomError("error", code=400) + + assert exc_info.value.code == 400 + assert "error" in str(exc_info.value) +``` + +## Testing Side Effects + +### Testing File Operations + +```python +import tempfile +import os + +def test_file_processing(): + """Test file processing with temp file.""" + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f: + f.write("test content") + temp_path = f.name + + try: + result = process_file(temp_path) + assert result == "processed: test content" + finally: + os.unlink(temp_path) +``` + +### Testing with pytest's tmp_path Fixture + +```python +def test_with_tmp_path(tmp_path): + """Test using pytest's built-in temp path fixture.""" + test_file = tmp_path / "test.txt" + test_file.write_text("hello world") + + result = process_file(str(test_file)) + assert result == "hello world" + # tmp_path automatically cleaned up +``` + +### Testing with tmpdir Fixture + +```python +def test_with_tmpdir(tmpdir): + """Test using pytest's tmpdir fixture.""" + test_file = tmpdir.join("test.txt") + test_file.write("data") + + result = process_file(str(test_file)) + assert result == "data" +``` + +## Test Organization + +### Directory Structure + +``` +tests/ +├── conftest.py # Shared fixtures +├── __init__.py +├── unit/ # Unit tests +│ ├── __init__.py +│ ├── test_models.py +│ ├── test_utils.py +│ └── test_services.py +├── integration/ # Integration tests +│ ├── __init__.py +│ ├── test_api.py +│ └── test_database.py +└── e2e/ # End-to-end tests + ├── __init__.py + └── test_user_flow.py +``` + +### Test Classes + +```python +class TestUserService: + """Group related tests in a class.""" + + @pytest.fixture(autouse=True) + def setup(self): + """Setup runs before each test in this class.""" + self.service = UserService() + + def test_create_user(self): + """Test user creation.""" + user = self.service.create_user("Alice") + assert user.name == "Alice" + + def test_delete_user(self): + """Test user deletion.""" + user = User(id=1, name="Bob") + self.service.delete_user(user) + assert not self.service.user_exists(1) +``` + +## Best Practices + +### DO + +- **Follow TDD**: Write tests before code (red-green-refactor) +- **Test one thing**: Each test should verify a single behavior +- **Use descriptive names**: `test_user_login_with_invalid_credentials_fails` +- **Use fixtures**: Eliminate duplication with fixtures +- **Mock external dependencies**: Don't depend on external services +- **Test edge cases**: Empty inputs, None values, boundary conditions +- **Aim for 80%+ coverage**: Focus on critical paths +- **Keep tests fast**: Use marks to separate slow tests + +### DON'T + +- **Don't test implementation**: Test behavior, not internals +- **Don't use complex conditionals in tests**: Keep tests simple +- **Don't ignore test failures**: All tests must pass +- **Don't test third-party code**: Trust libraries to work +- **Don't share state between tests**: Tests should be independent +- **Don't catch exceptions in tests**: Use `pytest.raises` +- **Don't use print statements**: Use assertions and pytest output +- **Don't write tests that are too brittle**: Avoid over-specific mocks + +## Common Patterns + +### Testing API Endpoints (FastAPI/Flask) + +```python +@pytest.fixture +def client(): + app = create_app(testing=True) + return app.test_client() + +def test_get_user(client): + response = client.get("/api/users/1") + assert response.status_code == 200 + assert response.json["id"] == 1 + +def test_create_user(client): + response = client.post("/api/users", json={ + "name": "Alice", + "email": "alice@example.com" + }) + assert response.status_code == 201 + assert response.json["name"] == "Alice" +``` + +### Testing Database Operations + +```python +@pytest.fixture +def db_session(): + """Create a test database session.""" + session = Session(bind=engine) + session.begin_nested() + yield session + session.rollback() + session.close() + +def test_create_user(db_session): + user = User(name="Alice", email="alice@example.com") + db_session.add(user) + db_session.commit() + + retrieved = db_session.query(User).filter_by(name="Alice").first() + assert retrieved.email == "alice@example.com" +``` + +### Testing Class Methods + +```python +class TestCalculator: + @pytest.fixture + def calculator(self): + return Calculator() + + def test_add(self, calculator): + assert calculator.add(2, 3) == 5 + + def test_divide_by_zero(self, calculator): + with pytest.raises(ZeroDivisionError): + calculator.divide(10, 0) +``` + +## pytest Configuration + +### pytest.ini + +```ini +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + --strict-markers + --disable-warnings + --cov=mypackage + --cov-report=term-missing + --cov-report=html +markers = + slow: marks tests as slow + integration: marks tests as integration tests + unit: marks tests as unit tests +``` + +### pyproject.toml + +```toml +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "--strict-markers", + "--cov=mypackage", + "--cov-report=term-missing", + "--cov-report=html", +] +markers = [ + "slow: marks tests as slow", + "integration: marks tests as integration tests", + "unit: marks tests as unit tests", +] +``` + +## Running Tests + +```bash +# Run all tests +pytest + +# Run specific file +pytest tests/test_utils.py + +# Run specific test +pytest tests/test_utils.py::test_function + +# Run with verbose output +pytest -v + +# Run with coverage +pytest --cov=mypackage --cov-report=html + +# Run only fast tests +pytest -m "not slow" + +# Run until first failure +pytest -x + +# Run and stop on N failures +pytest --maxfail=3 + +# Run last failed tests +pytest --lf + +# Run tests with pattern +pytest -k "test_user" + +# Run with debugger on failure +pytest --pdb +``` + +## Quick Reference + +| Pattern | Usage | +|---------|-------| +| `pytest.raises()` | Test expected exceptions | +| `@pytest.fixture()` | Create reusable test fixtures | +| `@pytest.mark.parametrize()` | Run tests with multiple inputs | +| `@pytest.mark.slow` | Mark slow tests | +| `pytest -m "not slow"` | Skip slow tests | +| `@patch()` | Mock functions and classes | +| `tmp_path` fixture | Automatic temp directory | +| `pytest --cov` | Generate coverage report | +| `assert` | Simple and readable assertions | + +**Remember**: Tests are code too. Keep them clean, readable, and maintainable. Good tests catch bugs; great tests prevent them. diff --git a/.agents/skills/hermes-security-auditor/SKILL.md b/.agents/skills/hermes-security-auditor/SKILL.md new file mode 100644 index 0000000..fc397f0 --- /dev/null +++ b/.agents/skills/hermes-security-auditor/SKILL.md @@ -0,0 +1,399 @@ +--- +name: security-auditor +version: 1.0.0 +description: Use when reviewing code for security vulnerabilities, implementing authentication flows, auditing OWASP Top 10, configuring CORS/CSP headers, handling secrets, input validation, SQL injection prevention, XSS protection, or any security-related code review. +triggers: + - security + - vulnerability + - OWASP + - XSS + - SQL injection + - CSRF + - CORS + - CSP + - authentication + - authorization + - encryption + - secrets + - JWT + - OAuth + - audit + - penetration + - sanitize + - validate input +role: specialist +scope: review +output-format: structured +--- + +# Security Auditor + +Comprehensive security audit and secure coding specialist. Adapted from buildwithclaude by Dave Poon (MIT). + +## Role Definition + +You are a senior application security engineer specializing in secure coding practices, vulnerability detection, and OWASP compliance. You conduct thorough security reviews and provide actionable fixes. + +## Audit Process + +1. **Conduct comprehensive security audit** of code and architecture +2. **Identify vulnerabilities** using OWASP Top 10 framework +3. **Design secure authentication and authorization** flows +4. **Implement input validation** and encryption mechanisms +5. **Create security tests** and monitoring strategies + +## Core Principles + +- Apply defense in depth with multiple security layers +- Follow principle of least privilege for all access controls +- Never trust user input — validate everything rigorously +- Design systems to fail securely without information leakage +- Conduct regular dependency scanning and updates +- Focus on practical fixes over theoretical security risks + +--- + +## OWASP Top 10 Checklist + +### 1. Broken Access Control (A01:2021) + +```typescript +// ❌ BAD: No authorization check +app.delete('/api/posts/:id', async (req, res) => { + await db.post.delete({ where: { id: req.params.id } }) + res.json({ success: true }) +}) + +// ✅ GOOD: Verify ownership +app.delete('/api/posts/:id', authenticate, async (req, res) => { + const post = await db.post.findUnique({ where: { id: req.params.id } }) + if (!post) return res.status(404).json({ error: 'Not found' }) + if (post.authorId !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ error: 'Forbidden' }) + } + await db.post.delete({ where: { id: req.params.id } }) + res.json({ success: true }) +}) +``` + +**Checks:** +- [ ] Every endpoint verifies authentication +- [ ] Every data access verifies authorization (ownership or role) +- [ ] CORS configured with specific origins (not `*` in production) +- [ ] Directory listing disabled +- [ ] Rate limiting on sensitive endpoints +- [ ] JWT tokens validated on every request + +### 2. Cryptographic Failures (A02:2021) + +```typescript +// ❌ BAD: Storing plaintext passwords +await db.user.create({ data: { password: req.body.password } }) + +// ✅ GOOD: Bcrypt with sufficient rounds +import bcrypt from 'bcryptjs' +const hashedPassword = await bcrypt.hash(req.body.password, 12) +await db.user.create({ data: { password: hashedPassword } }) +``` + +**Checks:** +- [ ] Passwords hashed with bcrypt (12+ rounds) or argon2 +- [ ] Sensitive data encrypted at rest (AES-256) +- [ ] TLS/HTTPS enforced for all connections +- [ ] No secrets in source code or logs +- [ ] API keys rotated regularly +- [ ] Sensitive fields excluded from API responses + +### 3. Injection (A03:2021) + +```typescript +// ❌ BAD: SQL injection vulnerable +const query = `SELECT * FROM users WHERE email = '${email}'` + +// ✅ GOOD: Parameterized queries +const user = await db.query('SELECT * FROM users WHERE email = $1', [email]) + +// ✅ GOOD: ORM with parameterized input +const user = await prisma.user.findUnique({ where: { email } }) +``` + +```typescript +// ❌ BAD: Command injection +const result = exec(`ls ${userInput}`) + +// ✅ GOOD: Use execFile with argument array +import { execFile } from 'child_process' +execFile('ls', [sanitizedPath], callback) +``` + +**Checks:** +- [ ] All database queries use parameterized statements or ORM +- [ ] No string concatenation in queries +- [ ] OS command execution uses argument arrays, not shell strings +- [ ] LDAP, XPath, and NoSQL injection prevented +- [ ] User input never used in `eval()`, `Function()`, or template literals for code + +### 4. Cross-Site Scripting (XSS) (A07:2021) + +```typescript +// ❌ BAD: dangerouslySetInnerHTML with user input +
+ +// ✅ GOOD: Sanitize HTML +import DOMPurify from 'isomorphic-dompurify' +
+ +// ✅ BEST: Render as text (React auto-escapes) +
{userComment}
+``` + +**Checks:** +- [ ] React auto-escaping relied upon (avoid `dangerouslySetInnerHTML`) +- [ ] If HTML rendering needed, sanitize with DOMPurify +- [ ] CSP headers configured (see below) +- [ ] HttpOnly cookies for session tokens +- [ ] URL parameters validated before rendering + +### 5. Security Misconfiguration (A05:2021) + +**Checks:** +- [ ] Default credentials changed +- [ ] Error messages don't leak stack traces in production +- [ ] Unnecessary HTTP methods disabled +- [ ] Security headers configured (see below) +- [ ] Debug mode disabled in production +- [ ] Dependencies up to date (`npm audit`) + +--- + +## Security Headers + +```typescript +// next.config.js +const securityHeaders = [ + { key: 'X-DNS-Prefetch-Control', value: 'on' }, + { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, + { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, + { key: 'X-Content-Type-Options', value: 'nosniff' }, + { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, + { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, + { + key: 'Content-Security-Policy', + value: [ + "default-src 'self'", + "script-src 'self' 'unsafe-eval' 'unsafe-inline'", // tighten in production + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: https:", + "font-src 'self'", + "connect-src 'self' https://api.example.com", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", + ].join('; '), + }, +] + +module.exports = { + async headers() { + return [{ source: '/(.*)', headers: securityHeaders }] + }, +} +``` + +--- + +## Input Validation Patterns + +### Zod Validation for API/Actions + +```typescript +import { z } from 'zod' + +const userSchema = z.object({ + email: z.string().email().max(255), + password: z.string().min(8).max(128), + name: z.string().min(1).max(100).regex(/^[a-zA-Z\s'-]+$/), + age: z.number().int().min(13).max(150).optional(), +}) + +// Server Action +export async function createUser(formData: FormData) { + 'use server' + const parsed = userSchema.safeParse({ + email: formData.get('email'), + password: formData.get('password'), + name: formData.get('name'), + }) + + if (!parsed.success) { + return { error: parsed.error.flatten() } + } + + // Safe to use parsed.data +} +``` + +### File Upload Validation + +```typescript +const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'] +const MAX_SIZE = 5 * 1024 * 1024 // 5MB + +export async function uploadFile(formData: FormData) { + 'use server' + const file = formData.get('file') as File + + if (!file || file.size === 0) return { error: 'No file' } + if (!ALLOWED_TYPES.includes(file.type)) return { error: 'Invalid file type' } + if (file.size > MAX_SIZE) return { error: 'File too large' } + + // Read and validate magic bytes, not just extension + const bytes = new Uint8Array(await file.arrayBuffer()) + if (!validateMagicBytes(bytes, file.type)) return { error: 'File content mismatch' } +} +``` + +--- + +## Authentication Security + +### JWT Best Practices + +```typescript +import { SignJWT, jwtVerify } from 'jose' + +const secret = new TextEncoder().encode(process.env.JWT_SECRET) // min 256-bit + +export async function createToken(payload: { userId: string; role: string }) { + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('15m') // Short-lived access tokens + .setAudience('your-app') + .setIssuer('your-app') + .sign(secret) +} + +export async function verifyToken(token: string) { + try { + const { payload } = await jwtVerify(token, secret, { + algorithms: ['HS256'], + audience: 'your-app', + issuer: 'your-app', + }) + return payload + } catch { + return null + } +} +``` + +### Cookie Security + +```typescript +cookies().set('session', token, { + httpOnly: true, // No JavaScript access + secure: true, // HTTPS only + sameSite: 'lax', // CSRF protection + maxAge: 60 * 60 * 24 * 7, + path: '/', +}) +``` + +### Rate Limiting + +```typescript +import { Ratelimit } from '@upstash/ratelimit' +import { Redis } from '@upstash/redis' + +const ratelimit = new Ratelimit({ + redis: Redis.fromEnv(), + limiter: Ratelimit.slidingWindow(10, '10 s'), +}) + +// In middleware or route handler +const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1' +const { success, remaining } = await ratelimit.limit(ip) +if (!success) { + return NextResponse.json({ error: 'Too many requests' }, { status: 429 }) +} +``` + +--- + +## Environment & Secrets + +```typescript +// ❌ BAD +const API_KEY = 'sk-1234567890abcdef' + +// ✅ GOOD +const API_KEY = process.env.API_KEY +if (!API_KEY) throw new Error('API_KEY not configured') +``` + +**Rules:** +- Never commit `.env` files (only `.env.example` with placeholder values) +- Use different secrets per environment +- Rotate secrets regularly +- Use a secrets manager (Vault, AWS SSM, Doppler) for production +- Never log secrets or include them in error responses + +--- + +## Dependency Security + +```bash +# Regular audit +npm audit +npm audit fix + +# Check for known vulnerabilities +npx better-npm-audit audit + +# Keep dependencies updated +npx npm-check-updates -u +``` + +--- + +## Security Audit Report Format + +When conducting a review, output findings as: + +``` +## Security Audit Report + +### Critical (Must Fix) +1. **[A03:Injection]** SQL injection in `/api/search` — user input concatenated into query + - File: `app/api/search/route.ts:15` + - Fix: Use parameterized query + - Risk: Full database compromise + +### High (Should Fix) +1. **[A01:Access Control]** Missing auth check on DELETE endpoint + - File: `app/api/posts/[id]/route.ts:42` + - Fix: Add authentication middleware and ownership check + +### Medium (Recommended) +1. **[A05:Misconfiguration]** Missing security headers + - Fix: Add CSP, HSTS, X-Frame-Options headers + +### Low (Consider) +1. **[A06:Vulnerable Components]** 3 packages with known vulnerabilities + - Run: `npm audit fix` +``` + +--- + +## Protected File Patterns + +These files should be reviewed carefully before any modification: + +- `.env*` — environment secrets +- `auth.ts` / `auth.config.ts` — authentication configuration +- `middleware.ts` — route protection logic +- `**/api/auth/**` — auth endpoints +- `prisma/schema.prisma` — database schema (permissions, RLS) +- `next.config.*` — security headers, redirects +- `package.json` / `package-lock.json` — dependency changes diff --git a/.agents/skills/hermes-security-auditor/_meta.json b/.agents/skills/hermes-security-auditor/_meta.json new file mode 100644 index 0000000..505fc88 --- /dev/null +++ b/.agents/skills/hermes-security-auditor/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn783g0k8dgj2wyz27c0e9he9180ce6k", + "slug": "security-auditor", + "version": "1.0.0", + "publishedAt": 1770008217892 +} \ No newline at end of file diff --git a/.agents/skills/hermes-ui-ux-pro-max/SKILL.md b/.agents/skills/hermes-ui-ux-pro-max/SKILL.md new file mode 100644 index 0000000..b94ef59 --- /dev/null +++ b/.agents/skills/hermes-ui-ux-pro-max/SKILL.md @@ -0,0 +1,43 @@ +--- +name: ui-ux-pro-max +description: UI/UX design intelligence and implementation guidance for building polished interfaces. Use when the user asks for UI design, UX flows, information architecture, visual style direction, design systems/tokens, component specs, copy/microcopy, accessibility, or to generate/critique/refine frontend UI (HTML/CSS/JS, React, Next.js, Vue, Svelte, Tailwind). Includes workflows for (1) generating new UI layouts and styling, (2) improving existing UI/UX, (3) producing design-system tokens and component guidelines, and (4) turning UX recommendations into concrete code changes. +--- + +Follow these steps to deliver high-quality UI/UX output with minimal back-and-forth. + +## 1) Triage +Ask only what you must to avoid wrong work: +- Target platform: web / iOS / Android / desktop +- Stack (if code changes): React/Next/Vue/Svelte, CSS/Tailwind, component library +- Goal and constraints: conversion, speed, brand vibe, accessibility level (WCAG AA?) +- What you have: screenshot, Figma, repo, URL, user journey + +If the user says "全部都要" (design + UX + code + design system), treat it as four deliverables and ship in that order. + +## 2) Produce Deliverables (pick what fits) +Always be concrete: name components, states, spacing, typography, and interactions. + +- **UI concept + layout**: Provide a clear visual direction, grid, typography, color system, key screens/sections. +- **UX flow**: Map the user journey, critical paths, error/empty/loading states, edge cases. +- **Design system**: Tokens (color/typography/spacing/radius/shadow), component rules, accessibility notes. +- **Implementation plan**: Exact file-level edits, component breakdown, and acceptance criteria. + +## 3) Use Bundled Assets +This skill bundles data you can cite for inspiration/standards. + +- **Design intelligence data**: Read from `skills/ui-ux-pro-max/assets/data/` when you need palettes, patterns, or UI/UX heuristics. +- **Upstream reference**: If you need more phrasing/examples, consult `skills/ui-ux-pro-max/references/upstream-skill-content.md`. + +## 4) Optional Script (Design System Generator) +If you need to quickly generate tokens and page-specific overrides, use the bundled script: + +```bash +python3 skills/ui-ux-pro-max/scripts/design_system.py --help +``` + +Prefer running it when the user wants a structured token output (ASCII-friendly). + +## Output Standards +- Default to ASCII-only tokens/variables unless the project already uses Unicode. +- Include: spacing scale, type scale, 2-3 font pair options, color tokens, component states. +- Always cover: empty/loading/error, keyboard navigation, focus states, contrast. diff --git a/.agents/skills/hermes-ui-ux-pro-max/_meta.json b/.agents/skills/hermes-ui-ux-pro-max/_meta.json new file mode 100644 index 0000000..71bf465 --- /dev/null +++ b/.agents/skills/hermes-ui-ux-pro-max/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn7dzk42mb9ceh8sm1zdx51sws803c9b", + "slug": "ui-ux-pro-max", + "version": "0.1.0", + "publishedAt": 1769616159343 +} \ No newline at end of file diff --git a/.agents/skills/hermes-ui-ux-pro-max/assets/data/charts.csv b/.agents/skills/hermes-ui-ux-pro-max/assets/data/charts.csv new file mode 100644 index 0000000..9463c37 --- /dev/null +++ b/.agents/skills/hermes-ui-ux-pro-max/assets/data/charts.csv @@ -0,0 +1,26 @@ +No,Data Type,Keywords,Best Chart Type,Secondary Options,Color Guidance,Performance Impact,Accessibility Notes,Library Recommendation,Interactive Level +1,Trend Over Time,"trend, time-series, line, growth, timeline, progress",Line Chart,"Area Chart, Smooth Area",Primary: #0080FF. Multiple series: use distinct colors. Fill: 20% opacity,⚡ Excellent (optimized),✓ Clear line patterns for colorblind users. Add pattern overlays.,"Chart.js, Recharts, ApexCharts",Hover + Zoom +2,Compare Categories,"compare, categories, bar, comparison, ranking",Bar Chart (Horizontal or Vertical),"Column Chart, Grouped Bar",Each bar: distinct color. Category: grouped same color. Sorted: descending order,⚡ Excellent,✓ Easy to compare. Add value labels on bars for clarity.,"Chart.js, Recharts, D3.js",Hover + Sort +3,Part-to-Whole,"part-to-whole, pie, donut, percentage, proportion, share",Pie Chart or Donut,"Stacked Bar, Treemap",Colors: 5-6 max. Contrasting palette. Large slices first. Use labels.,⚡ Good (limit 6 slices),⚠ Hard for accessibility. Better: Stacked bar with legend. Avoid pie if >5 items.,"Chart.js, Recharts, D3.js",Hover + Drill +4,Correlation/Distribution,"correlation, distribution, scatter, relationship, pattern",Scatter Plot or Bubble Chart,"Heat Map, Matrix",Color axis: gradient (blue-red). Size: relative. Opacity: 0.6-0.8 to show density,⚠ Moderate (many points),⚠ Provide data table alternative. Use pattern + color distinction.,"D3.js, Plotly, Recharts",Hover + Brush +5,Heatmap/Intensity,"heatmap, heat-map, intensity, density, matrix",Heat Map or Choropleth,"Grid Heat Map, Bubble Heat",Gradient: Cool (blue) to Hot (red). Scale: clear legend. Divergent for ±data,⚡ Excellent (color CSS),⚠ Colorblind: Use pattern overlay. Provide numerical legend.,"D3.js, Plotly, ApexCharts",Hover + Zoom +6,Geographic Data,"geographic, map, location, region, geo, spatial","Choropleth Map, Bubble Map",Geographic Heat Map,Regional: single color gradient or categorized colors. Legend: clear scale,⚠ Moderate (rendering),⚠ Include text labels for regions. Provide data table alternative.,"D3.js, Mapbox, Leaflet",Pan + Zoom + Drill +7,Funnel/Flow,funnel/flow,"Funnel Chart, Sankey",Waterfall (for flows),Stages: gradient (starting color → ending color). Show conversion %,⚡ Good,✓ Clear stage labels + percentages. Good for accessibility if labeled.,"D3.js, Recharts, Custom SVG",Hover + Drill +8,Performance vs Target,performance-vs-target,Gauge Chart or Bullet Chart,"Dial, Thermometer",Performance: Red→Yellow→Green gradient. Target: marker line. Threshold colors,⚡ Good,✓ Add numerical value + percentage label beside gauge.,"D3.js, ApexCharts, Custom SVG",Hover +9,Time-Series Forecast,time-series-forecast,Line with Confidence Band,Ribbon Chart,Actual: solid line #0080FF. Forecast: dashed #FF9500. Band: light shading,⚡ Good,✓ Clearly distinguish actual vs forecast. Add legend.,"Chart.js, ApexCharts, Plotly",Hover + Toggle +10,Anomaly Detection,anomaly-detection,Line Chart with Highlights,Scatter with Alert,Normal: blue #0080FF. Anomaly: red #FF0000 circle/square marker + alert,⚡ Good,✓ Circle/marker for anomalies. Add text alert annotation.,"D3.js, Plotly, ApexCharts",Hover + Alert +11,Hierarchical/Nested Data,hierarchical/nested-data,Treemap,"Sunburst, Nested Donut, Icicle",Parent: distinct hues. Children: lighter shades. White borders 2-3px.,⚠ Moderate,⚠ Poor - provide table alternative. Label large areas.,"D3.js, Recharts, ApexCharts",Hover + Drilldown +12,Flow/Process Data,flow/process-data,Sankey Diagram,"Alluvial, Chord Diagram",Gradient from source to target. Opacity 0.4-0.6 for flows.,⚠ Moderate,⚠ Poor - provide flow table alternative.,"D3.js (d3-sankey), Plotly",Hover + Drilldown +13,Cumulative Changes,cumulative-changes,Waterfall Chart,"Stacked Bar, Cascade",Increases: #4CAF50. Decreases: #F44336. Start: #2196F3. End: #0D47A1.,⚡ Good,✓ Good - clear directional colors with labels.,"ApexCharts, Highcharts, Plotly",Hover +14,Multi-Variable Comparison,multi-variable-comparison,Radar/Spider Chart,"Parallel Coordinates, Grouped Bar",Single: #0080FF 20% fill. Multiple: distinct colors per dataset.,⚡ Good,⚠ Moderate - limit 5-8 axes. Add data table.,"Chart.js, Recharts, ApexCharts",Hover + Toggle +15,Stock/Trading OHLC,stock/trading-ohlc,Candlestick Chart,"OHLC Bar, Heikin-Ashi",Bullish: #26A69A. Bearish: #EF5350. Volume: 40% opacity below.,⚡ Good,⚠ Moderate - provide OHLC data table.,"Lightweight Charts (TradingView), ApexCharts",Real-time + Hover + Zoom +16,Relationship/Connection Data,relationship/connection-data,Network Graph,"Hierarchical Tree, Adjacency Matrix",Node types: categorical colors. Edges: #90A4AE 60% opacity.,❌ Poor (500+ nodes struggles),❌ Very Poor - provide adjacency list alternative.,"D3.js (d3-force), Vis.js, Cytoscape.js",Drilldown + Hover + Drag +17,Distribution/Statistical,distribution/statistical,Box Plot,"Violin Plot, Beeswarm",Box: #BBDEFB. Border: #1976D2. Median: #D32F2F. Outliers: #F44336.,⚡ Excellent,"✓ Good - include stats table (min, Q1, median, Q3, max).","Plotly, D3.js, Chart.js (plugin)",Hover +18,Performance vs Target (Compact),performance-vs-target-(compact),Bullet Chart,"Gauge, Progress Bar","Ranges: #FFCDD2, #FFF9C4, #C8E6C9. Performance: #1976D2. Target: black 3px.",⚡ Excellent,✓ Excellent - compact with clear values.,"D3.js, Plotly, Custom SVG",Hover +19,Proportional/Percentage,proportional/percentage,Waffle Chart,"Pictogram, Stacked Bar 100%",10x10 grid. 3-5 categories max. 2-3px spacing between squares.,⚡ Good,✓ Good - better than pie for accessibility.,"D3.js, React-Waffle, Custom CSS Grid",Hover +20,Hierarchical Proportional,hierarchical-proportional,Sunburst Chart,"Treemap, Icicle, Circle Packing",Center to outer: darker to lighter. 15-20% lighter per level.,⚠ Moderate,⚠ Poor - provide hierarchy table alternative.,"D3.js (d3-hierarchy), Recharts, ApexCharts",Drilldown + Hover +21,Root Cause Analysis,"root cause, decomposition, tree, hierarchy, drill-down, ai-split",Decomposition Tree,"Decision Tree, Flow Chart",Nodes: #2563EB (Primary) vs #EF4444 (Negative impact). Connectors: Neutral grey.,⚠ Moderate (calculation heavy),✓ clear hierarchy. Allow keyboard navigation for nodes.,"Power BI (native), React-Flow, Custom D3.js",Drill + Expand +22,3D Spatial Data,"3d, spatial, immersive, terrain, molecular, volumetric",3D Scatter/Surface Plot,"Volumetric Rendering, Point Cloud",Depth cues: lighting/shading. Z-axis: color gradient (cool to warm).,❌ Heavy (WebGL required),❌ Poor - requires alternative 2D view or data table.,"Three.js, Deck.gl, Plotly 3D",Rotate + Zoom + VR +23,Real-Time Streaming,"streaming, real-time, ticker, live, velocity, pulse",Streaming Area Chart,"Ticker Tape, Moving Gauge",Current: Bright Pulse (#00FF00). History: Fading opacity. Grid: Dark.,⚡ Optimized (canvas/webgl),⚠ Flashing elements - provide pause button. High contrast.,Smoothed D3.js, CanvasJS +24,Sentiment/Emotion,"sentiment, emotion, nlp, opinion, feeling",Word Cloud with Sentiment,"Sentiment Arc, Radar Chart",Positive: #22C55E. Negative: #EF4444. Neutral: #94A3B8. Size = Frequency.,⚡ Good,⚠ Word clouds poor for screen readers. Use list view.,"D3-cloud, Highcharts, Nivo",Hover + Filter +25,Process Mining,"process, mining, variants, path, bottleneck, log",Process Map / Graph,"Directed Acyclic Graph (DAG), Petri Net",Happy path: #10B981 (Thick). Deviations: #F59E0B (Thin). Bottlenecks: #EF4444.,⚠ Moderate to Heavy,⚠ Complex graphs hard to navigate. Provide path summary.,"React-Flow, Cytoscape.js, Recharts",Drag + Node-Click diff --git a/.agents/skills/hermes-ui-ux-pro-max/assets/data/colors.csv b/.agents/skills/hermes-ui-ux-pro-max/assets/data/colors.csv new file mode 100644 index 0000000..a187845 --- /dev/null +++ b/.agents/skills/hermes-ui-ux-pro-max/assets/data/colors.csv @@ -0,0 +1,97 @@ +No,Product Type,Primary (Hex),Secondary (Hex),CTA (Hex),Background (Hex),Text (Hex),Border (Hex),Notes +1,SaaS (General),#2563EB,#3B82F6,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust blue + orange CTA contrast +2,Micro SaaS,#6366F1,#818CF8,#10B981,#F5F3FF,#1E1B4B,#E0E7FF,Indigo primary + emerald CTA +3,E-commerce,#059669,#10B981,#F97316,#ECFDF5,#064E3B,#A7F3D0,Success green + urgency orange +4,E-commerce Luxury,#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Premium dark + gold accent +5,Service Landing Page,#0EA5E9,#38BDF8,#F97316,#F0F9FF,#0C4A6E,#BAE6FD,Sky blue trust + warm CTA +6,B2B Service,#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional navy + blue CTA +7,Financial Dashboard,#0F172A,#1E293B,#22C55E,#020617,#F8FAFC,#334155,Dark bg + green positive indicators +8,Analytics Dashboard,#1E40AF,#3B82F6,#F59E0B,#F8FAFC,#1E3A8A,#DBEAFE,Blue data + amber highlights +9,Healthcare App,#0891B2,#22D3EE,#059669,#ECFEFF,#164E63,#A5F3FC,Calm cyan + health green +10,Educational App,#4F46E5,#818CF8,#F97316,#EEF2FF,#1E1B4B,#C7D2FE,Playful indigo + energetic orange +11,Creative Agency,#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold pink + cyan accent +12,Portfolio/Personal,#18181B,#3F3F46,#2563EB,#FAFAFA,#09090B,#E4E4E7,Monochrome + blue accent +13,Gaming,#7C3AED,#A78BFA,#F43F5E,#0F0F23,#E2E8F0,#4C1D95,Neon purple + rose action +14,Government/Public Service,#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,High contrast navy + blue +15,Fintech/Crypto,#F59E0B,#FBBF24,#8B5CF6,#0F172A,#F8FAFC,#334155,Gold trust + purple tech +16,Social Media App,#E11D48,#FB7185,#2563EB,#FFF1F2,#881337,#FECDD3,Vibrant rose + engagement blue +17,Productivity Tool,#0D9488,#14B8A6,#F97316,#F0FDFA,#134E4A,#99F6E4,Teal focus + action orange +18,Design System/Component Library,#4F46E5,#6366F1,#F97316,#EEF2FF,#312E81,#C7D2FE,Indigo brand + doc hierarchy +19,AI/Chatbot Platform,#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,AI purple + cyan interactions +20,NFT/Web3 Platform,#8B5CF6,#A78BFA,#FBBF24,#0F0F23,#F8FAFC,#4C1D95,Purple tech + gold value +21,Creator Economy Platform,#EC4899,#F472B6,#F97316,#FDF2F8,#831843,#FBCFE8,Creator pink + engagement orange +22,Sustainability/ESG Platform,#059669,#10B981,#0891B2,#ECFDF5,#064E3B,#A7F3D0,Nature green + ocean blue +23,Remote Work/Collaboration Tool,#6366F1,#818CF8,#10B981,#F5F3FF,#312E81,#E0E7FF,Calm indigo + success green +24,Mental Health App,#8B5CF6,#C4B5FD,#10B981,#FAF5FF,#4C1D95,#EDE9FE,Calming lavender + wellness green +25,Pet Tech App,#F97316,#FB923C,#2563EB,#FFF7ED,#9A3412,#FED7AA,Playful orange + trust blue +26,Smart Home/IoT Dashboard,#1E293B,#334155,#22C55E,#0F172A,#F8FAFC,#475569,Dark tech + status green +27,EV/Charging Ecosystem,#0891B2,#22D3EE,#22C55E,#ECFEFF,#164E63,#A5F3FC,Electric cyan + eco green +28,Subscription Box Service,#D946EF,#E879F9,#F97316,#FDF4FF,#86198F,#F5D0FE,Excitement purple + urgency orange +29,Podcast Platform,#1E1B4B,#312E81,#F97316,#0F0F23,#F8FAFC,#4338CA,Dark audio + warm accent +30,Dating App,#E11D48,#FB7185,#F97316,#FFF1F2,#881337,#FECDD3,Romantic rose + warm orange +31,Micro-Credentials/Badges Platform,#0369A1,#0EA5E9,#CA8A04,#F0F9FF,#0C4A6E,#BAE6FD,Trust blue + achievement gold +32,Knowledge Base/Documentation,#475569,#64748B,#2563EB,#F8FAFC,#1E293B,#E2E8F0,Neutral grey + link blue +33,Hyperlocal Services,#059669,#10B981,#F97316,#ECFDF5,#064E3B,#A7F3D0,Location green + action orange +34,Beauty/Spa/Wellness Service,#EC4899,#F9A8D4,#8B5CF6,#FDF2F8,#831843,#FBCFE8,Soft pink + lavender luxury +35,Luxury/Premium Brand,#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Premium black + gold accent +36,Restaurant/Food Service,#DC2626,#F87171,#CA8A04,#FEF2F2,#450A0A,#FECACA,Appetizing red + warm gold +37,Fitness/Gym App,#F97316,#FB923C,#22C55E,#1F2937,#F8FAFC,#374151,Energy orange + success green +38,Real Estate/Property,#0F766E,#14B8A6,#0369A1,#F0FDFA,#134E4A,#99F6E4,Trust teal + professional blue +39,Travel/Tourism Agency,#0EA5E9,#38BDF8,#F97316,#F0F9FF,#0C4A6E,#BAE6FD,Sky blue + adventure orange +40,Hotel/Hospitality,#1E3A8A,#3B82F6,#CA8A04,#F8FAFC,#1E40AF,#BFDBFE,Luxury navy + gold service +41,Wedding/Event Planning,#DB2777,#F472B6,#CA8A04,#FDF2F8,#831843,#FBCFE8,Romantic pink + elegant gold +42,Legal Services,#1E3A8A,#1E40AF,#B45309,#F8FAFC,#0F172A,#CBD5E1,Authority navy + trust gold +43,Insurance Platform,#0369A1,#0EA5E9,#22C55E,#F0F9FF,#0C4A6E,#BAE6FD,Security blue + protected green +44,Banking/Traditional Finance,#0F172A,#1E3A8A,#CA8A04,#F8FAFC,#020617,#E2E8F0,Trust navy + premium gold +45,Online Course/E-learning,#0D9488,#2DD4BF,#F97316,#F0FDFA,#134E4A,#5EEAD4,Progress teal + achievement orange +46,Non-profit/Charity,#0891B2,#22D3EE,#F97316,#ECFEFF,#164E63,#A5F3FC,Compassion blue + action orange +47,Music Streaming,#1E1B4B,#4338CA,#22C55E,#0F0F23,#F8FAFC,#312E81,Dark audio + play green +48,Video Streaming/OTT,#0F0F23,#1E1B4B,#E11D48,#000000,#F8FAFC,#312E81,Cinema dark + play red +49,Job Board/Recruitment,#0369A1,#0EA5E9,#22C55E,#F0F9FF,#0C4A6E,#BAE6FD,Professional blue + success green +50,Marketplace (P2P),#7C3AED,#A78BFA,#22C55E,#FAF5FF,#4C1D95,#DDD6FE,Trust purple + transaction green +51,Logistics/Delivery,#2563EB,#3B82F6,#F97316,#EFF6FF,#1E40AF,#BFDBFE,Tracking blue + delivery orange +52,Agriculture/Farm Tech,#15803D,#22C55E,#CA8A04,#F0FDF4,#14532D,#BBF7D0,Earth green + harvest gold +53,Construction/Architecture,#64748B,#94A3B8,#F97316,#F8FAFC,#334155,#E2E8F0,Industrial grey + safety orange +54,Automotive/Car Dealership,#1E293B,#334155,#DC2626,#F8FAFC,#0F172A,#E2E8F0,Premium dark + action red +55,Photography Studio,#18181B,#27272A,#F8FAFC,#000000,#FAFAFA,#3F3F46,Pure black + white contrast +56,Coworking Space,#F59E0B,#FBBF24,#2563EB,#FFFBEB,#78350F,#FDE68A,Energetic amber + booking blue +57,Cleaning Service,#0891B2,#22D3EE,#22C55E,#ECFEFF,#164E63,#A5F3FC,Fresh cyan + clean green +58,Home Services (Plumber/Electrician),#1E40AF,#3B82F6,#F97316,#EFF6FF,#1E3A8A,#BFDBFE,Professional blue + urgent orange +59,Childcare/Daycare,#F472B6,#FBCFE8,#22C55E,#FDF2F8,#9D174D,#FCE7F3,Soft pink + safe green +60,Senior Care/Elderly,#0369A1,#38BDF8,#22C55E,#F0F9FF,#0C4A6E,#E0F2FE,Calm blue + reassuring green +61,Medical Clinic,#0891B2,#22D3EE,#22C55E,#F0FDFA,#134E4A,#CCFBF1,Medical teal + health green +62,Pharmacy/Drug Store,#15803D,#22C55E,#0369A1,#F0FDF4,#14532D,#BBF7D0,Pharmacy green + trust blue +63,Dental Practice,#0EA5E9,#38BDF8,#FBBF24,#F0F9FF,#0C4A6E,#BAE6FD,Fresh blue + smile yellow +64,Veterinary Clinic,#0D9488,#14B8A6,#F97316,#F0FDFA,#134E4A,#99F6E4,Caring teal + warm orange +65,Florist/Plant Shop,#15803D,#22C55E,#EC4899,#F0FDF4,#14532D,#BBF7D0,Natural green + floral pink +66,Bakery/Cafe,#92400E,#B45309,#F8FAFC,#FEF3C7,#78350F,#FDE68A,Warm brown + cream white +67,Coffee Shop,#78350F,#92400E,#FBBF24,#FEF3C7,#451A03,#FDE68A,Coffee brown + warm gold +68,Brewery/Winery,#7C2D12,#B91C1C,#CA8A04,#FEF2F2,#450A0A,#FECACA,Deep burgundy + craft gold +69,Airline,#1E3A8A,#3B82F6,#F97316,#EFF6FF,#1E40AF,#BFDBFE,Sky blue + booking orange +70,News/Media Platform,#DC2626,#EF4444,#1E40AF,#FEF2F2,#450A0A,#FECACA,Breaking red + link blue +71,Magazine/Blog,#18181B,#3F3F46,#EC4899,#FAFAFA,#09090B,#E4E4E7,Editorial black + accent pink +72,Freelancer Platform,#6366F1,#818CF8,#22C55E,#EEF2FF,#312E81,#C7D2FE,Creative indigo + hire green +73,Consulting Firm,#0F172A,#334155,#CA8A04,#F8FAFC,#020617,#E2E8F0,Authority navy + premium gold +74,Marketing Agency,#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold pink + creative cyan +75,Event Management,#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Excitement purple + action orange +76,Conference/Webinar Platform,#1E40AF,#3B82F6,#22C55E,#EFF6FF,#1E3A8A,#BFDBFE,Professional blue + join green +77,Membership/Community,#7C3AED,#A78BFA,#22C55E,#FAF5FF,#4C1D95,#DDD6FE,Community purple + join green +78,Newsletter Platform,#0369A1,#0EA5E9,#F97316,#F0F9FF,#0C4A6E,#BAE6FD,Trust blue + subscribe orange +79,Digital Products/Downloads,#6366F1,#818CF8,#22C55E,#EEF2FF,#312E81,#C7D2FE,Digital indigo + buy green +80,Church/Religious Organization,#7C3AED,#A78BFA,#CA8A04,#FAF5FF,#4C1D95,#DDD6FE,Spiritual purple + warm gold +81,Sports Team/Club,#DC2626,#EF4444,#FBBF24,#FEF2F2,#7F1D1D,#FECACA,Team red + championship gold +82,Museum/Gallery,#18181B,#27272A,#F8FAFC,#FAFAFA,#09090B,#E4E4E7,Gallery black + white space +83,Theater/Cinema,#1E1B4B,#312E81,#CA8A04,#0F0F23,#F8FAFC,#4338CA,Dramatic dark + spotlight gold +84,Language Learning App,#4F46E5,#818CF8,#22C55E,#EEF2FF,#312E81,#C7D2FE,Learning indigo + progress green +85,Coding Bootcamp,#0F172A,#1E293B,#22C55E,#020617,#F8FAFC,#334155,Terminal dark + success green +86,Cybersecurity Platform,#00FF41,#0D0D0D,#FF3333,#000000,#E0E0E0,#1F1F1F,Matrix green + alert red +87,Developer Tool / IDE,#1E293B,#334155,#22C55E,#0F172A,#F8FAFC,#475569,Code dark + run green +88,Biotech / Life Sciences,#0EA5E9,#0284C7,#10B981,#F0F9FF,#0C4A6E,#BAE6FD,DNA blue + life green +89,Space Tech / Aerospace,#F8FAFC,#94A3B8,#3B82F6,#0B0B10,#F8FAFC,#1E293B,Star white + launch blue +90,Architecture / Interior,#171717,#404040,#D4AF37,#FFFFFF,#171717,#E5E5E5,Minimal black + accent gold +91,Quantum Computing,#00FFFF,#7B61FF,#FF00FF,#050510,#E0E0FF,#333344,Quantum cyan + interference purple +92,Biohacking / Longevity,#FF4D4D,#4D94FF,#00E676,#F5F5F7,#1C1C1E,#E5E5EA,Bio red/blue + vitality green +93,Autonomous Systems,#00FF41,#008F11,#FF3333,#0D1117,#E6EDF3,#30363D,Terminal green + alert red +94,Generative AI Art,#18181B,#3F3F46,#EC4899,#FAFAFA,#09090B,#E4E4E7,Canvas neutral + creative pink +95,Spatial / Vision OS,#FFFFFF,#E5E5E5,#007AFF,#888888,#000000,#CCCCCC,Glass white + system blue +96,Climate Tech,#059669,#10B981,#FBBF24,#ECFDF5,#064E3B,#A7F3D0,Nature green + solar gold diff --git a/.agents/skills/hermes-ui-ux-pro-max/assets/data/icons.csv b/.agents/skills/hermes-ui-ux-pro-max/assets/data/icons.csv new file mode 100644 index 0000000..a85e97f --- /dev/null +++ b/.agents/skills/hermes-ui-ux-pro-max/assets/data/icons.csv @@ -0,0 +1,101 @@ +No,Category,Icon Name,Keywords,Library,Import Code,Usage,Best For,Style +1,Navigation,menu,hamburger menu navigation toggle bars,Lucide,import { Menu } from 'lucide-react',,Mobile navigation drawer toggle sidebar,Outline +2,Navigation,arrow-left,back previous return navigate,Lucide,import { ArrowLeft } from 'lucide-react',,Back button breadcrumb navigation,Outline +3,Navigation,arrow-right,next forward continue navigate,Lucide,import { ArrowRight } from 'lucide-react',,Forward button next step CTA,Outline +4,Navigation,chevron-down,dropdown expand accordion select,Lucide,import { ChevronDown } from 'lucide-react',,Dropdown toggle accordion header,Outline +5,Navigation,chevron-up,collapse close accordion minimize,Lucide,import { ChevronUp } from 'lucide-react',,Accordion collapse minimize,Outline +6,Navigation,home,homepage main dashboard start,Lucide,import { Home } from 'lucide-react',,Home navigation main page,Outline +7,Navigation,x,close cancel dismiss remove exit,Lucide,import { X } from 'lucide-react',,Modal close dismiss button,Outline +8,Navigation,external-link,open new tab external link,Lucide,import { ExternalLink } from 'lucide-react',,External link indicator,Outline +9,Action,plus,add create new insert,Lucide,import { Plus } from 'lucide-react',,Add button create new item,Outline +10,Action,minus,remove subtract decrease delete,Lucide,import { Minus } from 'lucide-react',,Remove item quantity decrease,Outline +11,Action,trash-2,delete remove discard bin,Lucide,import { Trash2 } from 'lucide-react',,Delete action destructive,Outline +12,Action,edit,pencil modify change update,Lucide,import { Edit } from 'lucide-react',,Edit button modify content,Outline +13,Action,save,disk store persist save,Lucide,import { Save } from 'lucide-react',,Save button persist changes,Outline +14,Action,download,export save file download,Lucide,import { Download } from 'lucide-react',,Download file export,Outline +15,Action,upload,import file attach upload,Lucide,import { Upload } from 'lucide-react',,Upload file import,Outline +16,Action,copy,duplicate clipboard paste,Lucide,import { Copy } from 'lucide-react',,Copy to clipboard,Outline +17,Action,share,social distribute send,Lucide,import { Share } from 'lucide-react',,Share button social,Outline +18,Action,search,find lookup filter query,Lucide,import { Search } from 'lucide-react',,Search input bar,Outline +19,Action,filter,sort refine narrow options,Lucide,import { Filter } from 'lucide-react',,Filter dropdown sort,Outline +20,Action,settings,gear cog preferences config,Lucide,import { Settings } from 'lucide-react',,Settings page configuration,Outline +21,Status,check,success done complete verified,Lucide,import { Check } from 'lucide-react',,Success state checkmark,Outline +22,Status,check-circle,success verified approved complete,Lucide,import { CheckCircle } from 'lucide-react',,Success badge verified,Outline +23,Status,x-circle,error failed cancel rejected,Lucide,import { XCircle } from 'lucide-react',,Error state failed,Outline +24,Status,alert-triangle,warning caution attention danger,Lucide,import { AlertTriangle } from 'lucide-react',,Warning message caution,Outline +25,Status,alert-circle,info notice information help,Lucide,import { AlertCircle } from 'lucide-react',,Info notice alert,Outline +26,Status,info,information help tooltip details,Lucide,import { Info } from 'lucide-react',,Information tooltip help,Outline +27,Status,loader,loading spinner processing wait,Lucide,import { Loader } from 'lucide-react',,Loading state spinner,Outline +28,Status,clock,time schedule pending wait,Lucide,import { Clock } from 'lucide-react',,Pending time schedule,Outline +29,Communication,mail,email message inbox letter,Lucide,import { Mail } from 'lucide-react',,Email contact inbox,Outline +30,Communication,message-circle,chat comment bubble conversation,Lucide,import { MessageCircle } from 'lucide-react',,Chat comment message,Outline +31,Communication,phone,call mobile telephone contact,Lucide,import { Phone } from 'lucide-react',,Phone contact call,Outline +32,Communication,send,submit dispatch message airplane,Lucide,import { Send } from 'lucide-react',,Send message submit,Outline +33,Communication,bell,notification alert ring reminder,Lucide,import { Bell } from 'lucide-react',,Notification bell alert,Outline +34,User,user,profile account person avatar,Lucide,import { User } from 'lucide-react',,User profile account,Outline +35,User,users,team group people members,Lucide,import { Users } from 'lucide-react',,Team group members,Outline +36,User,user-plus,add invite new member,Lucide,import { UserPlus } from 'lucide-react',,Add user invite,Outline +37,User,log-in,signin authenticate enter,Lucide,import { LogIn } from 'lucide-react',,Login signin,Outline +38,User,log-out,signout exit leave logout,Lucide,import { LogOut } from 'lucide-react',,Logout signout,Outline +39,Media,image,photo picture gallery thumbnail,Lucide,import { Image } from 'lucide-react',,Image photo gallery,Outline +40,Media,video,movie film play record,Lucide,import { Video } from 'lucide-react',