v1.4.9: Validation fixes, Profiler fix, Quest link duplicate fix, Ascension fixes

This commit is contained in:
Xurkon
2026-03-26 06:47:54 -05:00
parent 284f0c3794
commit 1fc9727fee
194 changed files with 21008 additions and 743 deletions
+106
View File
@@ -0,0 +1,106 @@
# Rules
## Structure
Rules are organized into a **common** layer plus **language-specific** directories:
```
rules/
├── common/ # Language-agnostic principles (always install)
│ ├── coding-style.md
│ ├── git-workflow.md
│ ├── testing.md
│ ├── performance.md
│ ├── patterns.md
│ ├── hooks.md
│ ├── agents.md
│ └── security.md
├── typescript/ # TypeScript/JavaScript specific
├── python/ # Python specific
├── golang/ # Go specific
├── swift/ # Swift specific
└── php/ # PHP specific
```
- **common/** contains universal principles — no language-specific code examples.
- **Language directories** extend the common rules with framework-specific patterns, tools, and code examples. Each file references its common counterpart.
## Installation
### Option 1: Install Script (Recommended)
```bash
# Install common + one or more language-specific rule sets
./install.sh typescript
./install.sh python
./install.sh golang
./install.sh swift
./install.sh php
# Install multiple languages at once
./install.sh typescript python
```
### Option 2: Manual Installation
> **Important:** Copy entire directories — do NOT flatten with `/*`.
> Common and language-specific directories contain files with the same names.
> Flattening them into one directory causes language-specific files to overwrite
> common rules, and breaks the relative `../common/` references used by
> language-specific files.
```bash
# Install common rules (required for all projects)
cp -r rules/common ~/.claude/rules/common
# Install language-specific rules based on your project's tech stack
cp -r rules/typescript ~/.claude/rules/typescript
cp -r rules/python ~/.claude/rules/python
cp -r rules/golang ~/.claude/rules/golang
cp -r rules/swift ~/.claude/rules/swift
cp -r rules/php ~/.claude/rules/php
# Attention ! ! ! Configure according to your actual project requirements; the configuration here is for reference only.
```
## Rules vs Skills
- **Rules** define standards, conventions, and checklists that apply broadly (e.g., "80% test coverage", "no hardcoded secrets").
- **Skills** (`skills/` directory) provide deep, actionable reference material for specific tasks (e.g., `python-patterns`, `golang-testing`).
Language-specific rule files reference relevant skills where appropriate. Rules tell you *what* to do; skills tell you *how* to do it.
## Adding a New Language
To add support for a new language (e.g., `rust/`):
1. Create a `rules/rust/` directory
2. Add files that extend the common rules:
- `coding-style.md` — formatting tools, idioms, error handling patterns
- `testing.md` — test framework, coverage tools, test organization
- `patterns.md` — language-specific design patterns
- `hooks.md` — PostToolUse hooks for formatters, linters, type checkers
- `security.md` — secret management, security scanning tools
3. Each file should start with:
```
> This file extends [common/xxx.md](../common/xxx.md) with <Language> specific content.
```
4. Reference existing skills if available, or create new ones under `skills/`.
## Rule Priority
When language-specific rules and common rules conflict, **language-specific rules take precedence** (specific overrides general). This follows the standard layered configuration pattern (similar to CSS specificity or `.gitignore` precedence).
- `rules/common/` defines universal defaults applicable to all projects.
- `rules/golang/`, `rules/python/`, `rules/swift/`, `rules/php/`, `rules/typescript/`, etc. override those defaults where language idioms differ.
### Example
`common/coding-style.md` recommends immutability as a default principle. A language-specific `golang/coding-style.md` can override this:
> Idiomatic Go uses pointer receivers for struct mutation — see [common/coding-style.md](../common/coding-style.md) for the general principle, but Go-idiomatic mutation is preferred here.
### Common rules with override notes
Rules in `rules/common/` that may be overridden by language-specific files are marked with:
> **Language note**: This rule may be overridden by language-specific rules for languages where this pattern is not idiomatic.
+50
View File
@@ -0,0 +1,50 @@
# Agent Orchestration
## Available Agents
Located in `~/.claude/agents/`:
| Agent | Purpose | When to Use |
|-------|---------|-------------|
| planner | Implementation planning | Complex features, refactoring |
| architect | System design | Architectural decisions |
| tdd-guide | Test-driven development | New features, bug fixes |
| code-reviewer | Code review | After writing code |
| security-reviewer | Security analysis | Before commits |
| build-error-resolver | Fix build errors | When build fails |
| e2e-runner | E2E testing | Critical user flows |
| refactor-cleaner | Dead code cleanup | Code maintenance |
| doc-updater | Documentation | Updating docs |
| rust-reviewer | Rust code review | Rust projects |
## Immediate Agent Usage
No user prompt needed:
1. Complex feature requests - Use **planner** agent
2. Code just written/modified - Use **code-reviewer** agent
3. Bug fix or new feature - Use **tdd-guide** agent
4. Architectural decision - Use **architect** agent
## Parallel Task Execution
ALWAYS use parallel Task execution for independent operations:
```markdown
# GOOD: Parallel execution
Launch 3 agents in parallel:
1. Agent 1: Security analysis of auth module
2. Agent 2: Performance review of cache system
3. Agent 3: Type checking of utilities
# BAD: Sequential when unnecessary
First agent 1, then agent 2, then agent 3
```
## Multi-Perspective Analysis
For complex problems, use split role sub-agents:
- Factual reviewer
- Senior engineer
- Security expert
- Consistency reviewer
- Redundancy checker
+48
View File
@@ -0,0 +1,48 @@
# Coding Style
## Immutability (CRITICAL)
ALWAYS create new objects, NEVER mutate existing ones:
```
// Pseudocode
WRONG: modify(original, field, value) → changes original in-place
CORRECT: update(original, field, value) → returns new copy with change
```
Rationale: Immutable data prevents hidden side effects, makes debugging easier, and enables safe concurrency.
## File Organization
MANY SMALL FILES > FEW LARGE FILES:
- High cohesion, low coupling
- 200-400 lines typical, 800 max
- Extract utilities from large modules
- Organize by feature/domain, not by type
## Error Handling
ALWAYS handle errors comprehensively:
- Handle errors explicitly at every level
- Provide user-friendly error messages in UI-facing code
- Log detailed error context on the server side
- Never silently swallow errors
## Input Validation
ALWAYS validate at system boundaries:
- Validate all user input before processing
- Use schema-based validation where available
- Fail fast with clear error messages
- Never trust external data (API responses, user input, file content)
## Code Quality Checklist
Before marking work complete:
- [ ] Code is readable and well-named
- [ ] Functions are small (<50 lines)
- [ ] Files are focused (<800 lines)
- [ ] No deep nesting (>4 levels)
- [ ] Proper error handling
- [ ] No hardcoded values (use constants or config)
- [ ] No mutation (immutable patterns used)
@@ -0,0 +1,38 @@
# Development Workflow
> This file extends [common/git-workflow.md](./git-workflow.md) with the full feature development process that happens before git operations.
The Feature Implementation Workflow describes the development pipeline: research, planning, TDD, code review, and then committing to git.
## Feature Implementation Workflow
0. **Research & Reuse** _(mandatory before any new implementation)_
- **GitHub code search first:** Run `gh search repos` and `gh search code` to find existing implementations, templates, and patterns before writing anything new.
- **Library docs second:** Use Context7 or primary vendor docs to confirm API behavior, package usage, and version-specific details before implementing.
- **Exa only when the first two are insufficient:** Use Exa for broader web research or discovery after GitHub search and primary docs.
- **Check package registries:** Search npm, PyPI, crates.io, and other registries before writing utility code. Prefer battle-tested libraries over hand-rolled solutions.
- **Search for adaptable implementations:** Look for open-source projects that solve 80%+ of the problem and can be forked, ported, or wrapped.
- Prefer adopting or porting a proven approach over writing net-new code when it meets the requirement.
1. **Plan First**
- Use **planner** agent to create implementation plan
- Generate planning docs before coding: PRD, architecture, system_design, tech_doc, task_list
- Identify dependencies and risks
- Break down into phases
2. **TDD Approach**
- Use **tdd-guide** agent
- Write tests first (RED)
- Implement to pass tests (GREEN)
- Refactor (IMPROVE)
- Verify 80%+ coverage
3. **Code Review**
- Use **code-reviewer** agent immediately after writing code
- Address CRITICAL and HIGH issues
- Fix MEDIUM issues when possible
4. **Commit & Push**
- Detailed commit messages
- Follow conventional commits format
- See [git-workflow.md](./git-workflow.md) for commit message format and PR process
+24
View File
@@ -0,0 +1,24 @@
# Git Workflow
## Commit Message Format
```
<type>: <description>
<optional body>
```
Types: feat, fix, refactor, docs, test, chore, perf, ci
Note: Attribution disabled globally via ~/.claude/settings.json.
## Pull Request Workflow
When creating PRs:
1. Analyze full commit history (not just latest commit)
2. Use `git diff [base-branch]...HEAD` to see all changes
3. Draft comprehensive PR summary
4. Include test plan with TODOs
5. Push with `-u` flag if new branch
> For the full development process (planning, TDD, code review) before git operations,
> see [development-workflow.md](./development-workflow.md).
+30
View File
@@ -0,0 +1,30 @@
# Hooks System
## Hook Types
- **PreToolUse**: Before tool execution (validation, parameter modification)
- **PostToolUse**: After tool execution (auto-format, checks)
- **Stop**: When session ends (final verification)
## Auto-Accept Permissions
Use with caution:
- Enable for trusted, well-defined plans
- Disable for exploratory work
- Never use dangerously-skip-permissions flag
- Configure `allowedTools` in `~/.claude.json` instead
## TodoWrite Best Practices
Use TodoWrite tool to:
- Track progress on multi-step tasks
- Verify understanding of instructions
- Enable real-time steering
- Show granular implementation steps
Todo list reveals:
- Out of order steps
- Missing items
- Extra unnecessary items
- Wrong granularity
- Misinterpreted requirements
+31
View File
@@ -0,0 +1,31 @@
# Common Patterns
## Skeleton Projects
When implementing new functionality:
1. Search for battle-tested skeleton projects
2. Use parallel agents to evaluate options:
- Security assessment
- Extensibility analysis
- Relevance scoring
- Implementation planning
3. Clone best match as foundation
4. Iterate within proven structure
## Design Patterns
### Repository Pattern
Encapsulate data access behind a consistent interface:
- Define standard operations: findAll, findById, create, update, delete
- Concrete implementations handle storage details (database, API, file, etc.)
- Business logic depends on the abstract interface, not the storage mechanism
- Enables easy swapping of data sources and simplifies testing with mocks
### API Response Format
Use a consistent envelope for all API responses:
- Include a success/status indicator
- Include the data payload (nullable on error)
- Include an error message field (nullable on success)
- Include metadata for paginated responses (total, page, limit)
+55
View File
@@ -0,0 +1,55 @@
# Performance Optimization
## Model Selection Strategy
**Haiku 4.5** (90% of Sonnet capability, 3x cost savings):
- Lightweight agents with frequent invocation
- Pair programming and code generation
- Worker agents in multi-agent systems
**Sonnet 4.6** (Best coding model):
- Main development work
- Orchestrating multi-agent workflows
- Complex coding tasks
**Opus 4.5** (Deepest reasoning):
- Complex architectural decisions
- Maximum reasoning requirements
- Research and analysis tasks
## Context Window Management
Avoid last 20% of context window for:
- Large-scale refactoring
- Feature implementation spanning multiple files
- Debugging complex interactions
Lower context sensitivity tasks:
- Single-file edits
- Independent utility creation
- Documentation updates
- Simple bug fixes
## Extended Thinking + Plan Mode
Extended thinking is enabled by default, reserving up to 31,999 tokens for internal reasoning.
Control extended thinking via:
- **Toggle**: Option+T (macOS) / Alt+T (Windows/Linux)
- **Config**: Set `alwaysThinkingEnabled` in `~/.claude/settings.json`
- **Budget cap**: `export MAX_THINKING_TOKENS=10000`
- **Verbose mode**: Ctrl+O to see thinking output
For complex tasks requiring deep reasoning:
1. Ensure extended thinking is enabled (on by default)
2. Enable **Plan Mode** for structured approach
3. Use multiple critique rounds for thorough analysis
4. Use split role sub-agents for diverse perspectives
## Build Troubleshooting
If build fails:
1. Use **build-error-resolver** agent
2. Analyze error messages
3. Fix incrementally
4. Verify after each fix
+29
View File
@@ -0,0 +1,29 @@
# Security Guidelines
## Mandatory Security Checks
Before ANY commit:
- [ ] No hardcoded secrets (API keys, passwords, tokens)
- [ ] All user inputs validated
- [ ] SQL injection prevention (parameterized queries)
- [ ] XSS prevention (sanitized HTML)
- [ ] CSRF protection enabled
- [ ] Authentication/authorization verified
- [ ] Rate limiting on all endpoints
- [ ] Error messages don't leak sensitive data
## Secret Management
- NEVER hardcode secrets in source code
- ALWAYS use environment variables or a secret manager
- Validate that required secrets are present at startup
- Rotate any secrets that may have been exposed
## Security Response Protocol
If security issue found:
1. STOP immediately
2. Use **security-reviewer** agent
3. Fix CRITICAL issues before continuing
4. Rotate any exposed secrets
5. Review entire codebase for similar issues
+29
View File
@@ -0,0 +1,29 @@
# Testing Requirements
## Minimum Test Coverage: 80%
Test Types (ALL required):
1. **Unit Tests** - Individual functions, utilities, components
2. **Integration Tests** - API endpoints, database operations
3. **E2E Tests** - Critical user flows (framework chosen per language)
## Test-Driven Development
MANDATORY workflow:
1. Write test first (RED)
2. Run test - it should FAIL
3. Write minimal implementation (GREEN)
4. Run test - it should PASS
5. Refactor (IMPROVE)
6. Verify coverage (80%+)
## Troubleshooting Test Failures
1. Use **tdd-guide** agent
2. Check test isolation
3. Verify mocks are correct
4. Fix implementation, not tests (unless tests are wrong)
## Agent Support
- **tdd-guide** - Use PROACTIVELY for new features, enforces write-tests-first
+44
View File
@@ -0,0 +1,44 @@
---
paths:
- "**/*.cpp"
- "**/*.hpp"
- "**/*.cc"
- "**/*.hh"
- "**/*.cxx"
- "**/*.h"
- "**/CMakeLists.txt"
---
# C++ Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with C++ specific content.
## Modern C++ (C++17/20/23)
- Prefer **modern C++ features** over C-style constructs
- Use `auto` when the type is obvious from context
- Use `constexpr` for compile-time constants
- Use structured bindings: `auto [key, value] = map_entry;`
## Resource Management
- **RAII everywhere** — no manual `new`/`delete`
- Use `std::unique_ptr` for exclusive ownership
- Use `std::shared_ptr` only when shared ownership is truly needed
- Use `std::make_unique` / `std::make_shared` over raw `new`
## Naming Conventions
- Types/Classes: `PascalCase`
- Functions/Methods: `snake_case` or `camelCase` (follow project convention)
- Constants: `kPascalCase` or `UPPER_SNAKE_CASE`
- Namespaces: `lowercase`
- Member variables: `snake_case_` (trailing underscore) or `m_` prefix
## Formatting
- Use **clang-format** — no style debates
- Run `clang-format -i <file>` before committing
## Reference
See skill: `cpp-coding-standards` for comprehensive C++ coding standards and guidelines.
+39
View File
@@ -0,0 +1,39 @@
---
paths:
- "**/*.cpp"
- "**/*.hpp"
- "**/*.cc"
- "**/*.hh"
- "**/*.cxx"
- "**/*.h"
- "**/CMakeLists.txt"
---
# C++ Hooks
> This file extends [common/hooks.md](../common/hooks.md) with C++ specific content.
## Build Hooks
Run these checks before committing C++ changes:
```bash
# Format check
clang-format --dry-run --Werror src/*.cpp src/*.hpp
# Static analysis
clang-tidy src/*.cpp -- -std=c++17
# Build
cmake --build build
# Tests
ctest --test-dir build --output-on-failure
```
## Recommended CI Pipeline
1. **clang-format** — formatting check
2. **clang-tidy** — static analysis
3. **cppcheck** — additional analysis
4. **cmake build** — compilation
5. **ctest** — test execution with sanitizers
+51
View File
@@ -0,0 +1,51 @@
---
paths:
- "**/*.cpp"
- "**/*.hpp"
- "**/*.cc"
- "**/*.hh"
- "**/*.cxx"
- "**/*.h"
- "**/CMakeLists.txt"
---
# C++ Patterns
> This file extends [common/patterns.md](../common/patterns.md) with C++ specific content.
## RAII (Resource Acquisition Is Initialization)
Tie resource lifetime to object lifetime:
```cpp
class FileHandle {
public:
explicit FileHandle(const std::string& path) : file_(std::fopen(path.c_str(), "r")) {}
~FileHandle() { if (file_) std::fclose(file_); }
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
private:
std::FILE* file_;
};
```
## Rule of Five/Zero
- **Rule of Zero**: Prefer classes that need no custom destructor, copy/move constructors, or assignments
- **Rule of Five**: If you define any of destructor/copy-ctor/copy-assign/move-ctor/move-assign, define all five
## Value Semantics
- Pass small/trivial types by value
- Pass large types by `const&`
- Return by value (rely on RVO/NRVO)
- Use move semantics for sink parameters
## Error Handling
- Use exceptions for exceptional conditions
- Use `std::optional` for values that may not exist
- Use `std::expected` (C++23) or result types for expected failures
## Reference
See skill: `cpp-coding-standards` for comprehensive C++ patterns and anti-patterns.
+51
View File
@@ -0,0 +1,51 @@
---
paths:
- "**/*.cpp"
- "**/*.hpp"
- "**/*.cc"
- "**/*.hh"
- "**/*.cxx"
- "**/*.h"
- "**/CMakeLists.txt"
---
# C++ Security
> This file extends [common/security.md](../common/security.md) with C++ specific content.
## Memory Safety
- Never use raw `new`/`delete` — use smart pointers
- Never use C-style arrays — use `std::array` or `std::vector`
- Never use `malloc`/`free` — use C++ allocation
- Avoid `reinterpret_cast` unless absolutely necessary
## Buffer Overflows
- Use `std::string` over `char*`
- Use `.at()` for bounds-checked access when safety matters
- Never use `strcpy`, `strcat`, `sprintf` — use `std::string` or `fmt::format`
## Undefined Behavior
- Always initialize variables
- Avoid signed integer overflow
- Never dereference null or dangling pointers
- Use sanitizers in CI:
```bash
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" ..
```
## Static Analysis
- Use **clang-tidy** for automated checks:
```bash
clang-tidy --checks='*' src/*.cpp
```
- Use **cppcheck** for additional analysis:
```bash
cppcheck --enable=all src/
```
## Reference
See skill: `cpp-coding-standards` for detailed security guidelines.
+44
View File
@@ -0,0 +1,44 @@
---
paths:
- "**/*.cpp"
- "**/*.hpp"
- "**/*.cc"
- "**/*.hh"
- "**/*.cxx"
- "**/*.h"
- "**/CMakeLists.txt"
---
# C++ Testing
> This file extends [common/testing.md](../common/testing.md) with C++ specific content.
## Framework
Use **GoogleTest** (gtest/gmock) with **CMake/CTest**.
## Running Tests
```bash
cmake --build build && ctest --test-dir build --output-on-failure
```
## Coverage
```bash
cmake -DCMAKE_CXX_FLAGS="--coverage" -DCMAKE_EXE_LINKER_FLAGS="--coverage" ..
cmake --build .
ctest --output-on-failure
lcov --capture --directory . --output-file coverage.info
```
## Sanitizers
Always run tests with sanitizers in CI:
```bash
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" ..
```
## Reference
See skill: `cpp-testing` for detailed C++ testing patterns, TDD workflow, and GoogleTest/GMock usage.
+72
View File
@@ -0,0 +1,72 @@
---
paths:
- "**/*.cs"
- "**/*.csx"
---
# C# Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with C#-specific content.
## Standards
- Follow current .NET conventions and enable nullable reference types
- Prefer explicit access modifiers on public and internal APIs
- Keep files aligned with the primary type they define
## Types and Models
- Prefer `record` or `record struct` for immutable value-like models
- Use `class` for entities or types with identity and lifecycle
- Use `interface` for service boundaries and abstractions
- Avoid `dynamic` in application code; prefer generics or explicit models
```csharp
public sealed record UserDto(Guid Id, string Email);
public interface IUserRepository
{
Task<UserDto?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
}
```
## Immutability
- Prefer `init` setters, constructor parameters, and immutable collections for shared state
- Do not mutate input models in-place when producing updated state
```csharp
public sealed record UserProfile(string Name, string Email);
public static UserProfile Rename(UserProfile profile, string name) =>
profile with { Name = name };
```
## Async and Error Handling
- Prefer `async`/`await` over blocking calls like `.Result` or `.Wait()`
- Pass `CancellationToken` through public async APIs
- Throw specific exceptions and log with structured properties
```csharp
public async Task<Order> LoadOrderAsync(
Guid orderId,
CancellationToken cancellationToken)
{
try
{
return await repository.FindAsync(orderId, cancellationToken)
?? throw new InvalidOperationException($"Order {orderId} was not found.");
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to load order {OrderId}", orderId);
throw;
}
}
```
## Formatting
- Use `dotnet format` for formatting and analyzer fixes
- Keep `using` directives organized and remove unused imports
- Prefer expression-bodied members only when they stay readable
+25
View File
@@ -0,0 +1,25 @@
---
paths:
- "**/*.cs"
- "**/*.csx"
- "**/*.csproj"
- "**/*.sln"
- "**/Directory.Build.props"
- "**/Directory.Build.targets"
---
# C# Hooks
> This file extends [common/hooks.md](../common/hooks.md) with C#-specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **dotnet format**: Auto-format edited C# files and apply analyzer fixes
- **dotnet build**: Verify the solution or project still compiles after edits
- **dotnet test --no-build**: Re-run the nearest relevant test project after behavior changes
## Stop Hooks
- Run a final `dotnet build` before ending a session with broad C# changes
- Warn on modified `appsettings*.json` files so secrets do not get committed
+50
View File
@@ -0,0 +1,50 @@
---
paths:
- "**/*.cs"
- "**/*.csx"
---
# C# Patterns
> This file extends [common/patterns.md](../common/patterns.md) with C#-specific content.
## API Response Pattern
```csharp
public sealed record ApiResponse<T>(
bool Success,
T? Data = default,
string? Error = null,
object? Meta = null);
```
## Repository Pattern
```csharp
public interface IRepository<T>
{
Task<IReadOnlyList<T>> FindAllAsync(CancellationToken cancellationToken);
Task<T?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
Task<T> CreateAsync(T entity, CancellationToken cancellationToken);
Task<T> UpdateAsync(T entity, CancellationToken cancellationToken);
Task DeleteAsync(Guid id, CancellationToken cancellationToken);
}
```
## Options Pattern
Use strongly typed options for config instead of reading raw strings throughout the codebase.
```csharp
public sealed class PaymentsOptions
{
public const string SectionName = "Payments";
public required string BaseUrl { get; init; }
public required string ApiKeySecretName { get; init; }
}
```
## Dependency Injection
- Depend on interfaces at service boundaries
- Keep constructors focused; if a service needs too many dependencies, split responsibilities
- Register lifetimes intentionally: singleton for stateless/shared services, scoped for request data, transient for lightweight pure workers
+58
View File
@@ -0,0 +1,58 @@
---
paths:
- "**/*.cs"
- "**/*.csx"
- "**/*.csproj"
- "**/appsettings*.json"
---
# C# Security
> This file extends [common/security.md](../common/security.md) with C#-specific content.
## Secret Management
- Never hardcode API keys, tokens, or connection strings in source code
- Use environment variables, user secrets for local development, and a secret manager in production
- Keep `appsettings.*.json` free of real credentials
```csharp
// BAD
const string ApiKey = "sk-live-123";
// GOOD
var apiKey = builder.Configuration["OpenAI:ApiKey"]
?? throw new InvalidOperationException("OpenAI:ApiKey is not configured.");
```
## SQL Injection Prevention
- Always use parameterized queries with ADO.NET, Dapper, or EF Core
- Never concatenate user input into SQL strings
- Validate sort fields and filter operators before using dynamic query composition
```csharp
const string sql = "SELECT * FROM Orders WHERE CustomerId = @customerId";
await connection.QueryAsync<Order>(sql, new { customerId });
```
## Input Validation
- Validate DTOs at the application boundary
- Use data annotations, FluentValidation, or explicit guard clauses
- Reject invalid model state before running business logic
## Authentication and Authorization
- Prefer framework auth handlers instead of custom token parsing
- Enforce authorization policies at endpoint or handler boundaries
- Never log raw tokens, passwords, or PII
## Error Handling
- Return safe client-facing messages
- Log detailed exceptions with structured context server-side
- Do not expose stack traces, SQL text, or filesystem paths in API responses
## References
See skill: `security-review` for broader application security review checklists.
+46
View File
@@ -0,0 +1,46 @@
---
paths:
- "**/*.cs"
- "**/*.csx"
- "**/*.csproj"
---
# C# Testing
> This file extends [common/testing.md](../common/testing.md) with C#-specific content.
## Test Framework
- Prefer **xUnit** for unit and integration tests
- Use **FluentAssertions** for readable assertions
- Use **Moq** or **NSubstitute** for mocking dependencies
- Use **Testcontainers** when integration tests need real infrastructure
## Test Organization
- Mirror `src/` structure under `tests/`
- Separate unit, integration, and end-to-end coverage clearly
- Name tests by behavior, not implementation details
```csharp
public sealed class OrderServiceTests
{
[Fact]
public async Task FindByIdAsync_ReturnsOrder_WhenOrderExists()
{
// Arrange
// Act
// Assert
}
}
```
## ASP.NET Core Integration Tests
- Use `WebApplicationFactory<TEntryPoint>` for API integration coverage
- Test auth, validation, and serialization through HTTP, not by bypassing middleware
## Coverage
- Target 80%+ line coverage
- Focus coverage on domain logic, validation, auth, and failure paths
- Run `dotnet test` in CI with coverage collection enabled where available
+32
View File
@@ -0,0 +1,32 @@
---
paths:
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
---
# Go Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Go specific content.
## Formatting
- **gofmt** and **goimports** are mandatory — no style debates
## Design Principles
- Accept interfaces, return structs
- Keep interfaces small (1-3 methods)
## Error Handling
Always wrap errors with context:
```go
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
```
## Reference
See skill: `golang-patterns` for comprehensive Go idioms and patterns.
+17
View File
@@ -0,0 +1,17 @@
---
paths:
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
---
# Go Hooks
> This file extends [common/hooks.md](../common/hooks.md) with Go specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **gofmt/goimports**: Auto-format `.go` files after edit
- **go vet**: Run static analysis after editing `.go` files
- **staticcheck**: Run extended static checks on modified packages
+45
View File
@@ -0,0 +1,45 @@
---
paths:
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
---
# Go Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Go specific content.
## Functional Options
```go
type Option func(*Server)
func WithPort(port int) Option {
return func(s *Server) { s.port = port }
}
func NewServer(opts ...Option) *Server {
s := &Server{port: 8080}
for _, opt := range opts {
opt(s)
}
return s
}
```
## Small Interfaces
Define interfaces where they are used, not where they are implemented.
## Dependency Injection
Use constructor functions to inject dependencies:
```go
func NewUserService(repo UserRepository, logger Logger) *UserService {
return &UserService{repo: repo, logger: logger}
}
```
## Reference
See skill: `golang-patterns` for comprehensive Go patterns including concurrency, error handling, and package organization.
+34
View File
@@ -0,0 +1,34 @@
---
paths:
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
---
# Go Security
> This file extends [common/security.md](../common/security.md) with Go specific content.
## Secret Management
```go
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
log.Fatal("OPENAI_API_KEY not configured")
}
```
## Security Scanning
- Use **gosec** for static security analysis:
```bash
gosec ./...
```
## Context & Timeouts
Always use `context.Context` for timeout control:
```go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
```
+31
View File
@@ -0,0 +1,31 @@
---
paths:
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
---
# Go Testing
> This file extends [common/testing.md](../common/testing.md) with Go specific content.
## Framework
Use the standard `go test` with **table-driven tests**.
## Race Detection
Always run with the `-race` flag:
```bash
go test -race ./...
```
## Coverage
```bash
go test -cover ./...
```
## Reference
See skill: `golang-testing` for detailed Go testing patterns and helpers.
+114
View File
@@ -0,0 +1,114 @@
---
paths:
- "**/*.java"
---
# Java Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Java-specific content.
## Formatting
- **google-java-format** or **Checkstyle** (Google or Sun style) for enforcement
- One public top-level type per file
- Consistent indent: 2 or 4 spaces (match project standard)
- Member order: constants, fields, constructors, public methods, protected, private
## Immutability
- Prefer `record` for value types (Java 16+)
- Mark fields `final` by default — use mutable state only when required
- Return defensive copies from public APIs: `List.copyOf()`, `Map.copyOf()`, `Set.copyOf()`
- Copy-on-write: return new instances rather than mutating existing ones
```java
// GOOD — immutable value type
public record OrderSummary(Long id, String customerName, BigDecimal total) {}
// GOOD — final fields, no setters
public class Order {
private final Long id;
private final List<LineItem> items;
public List<LineItem> getItems() {
return List.copyOf(items);
}
}
```
## Naming
Follow standard Java conventions:
- `PascalCase` for classes, interfaces, records, enums
- `camelCase` for methods, fields, parameters, local variables
- `SCREAMING_SNAKE_CASE` for `static final` constants
- Packages: all lowercase, reverse domain (`com.example.app.service`)
## Modern Java Features
Use modern language features where they improve clarity:
- **Records** for DTOs and value types (Java 16+)
- **Sealed classes** for closed type hierarchies (Java 17+)
- **Pattern matching** with `instanceof` — no explicit cast (Java 16+)
- **Text blocks** for multi-line strings — SQL, JSON templates (Java 15+)
- **Switch expressions** with arrow syntax (Java 14+)
- **Pattern matching in switch** — exhaustive sealed type handling (Java 21+)
```java
// Pattern matching instanceof
if (shape instanceof Circle c) {
return Math.PI * c.radius() * c.radius();
}
// Sealed type hierarchy
public sealed interface PaymentMethod permits CreditCard, BankTransfer, Wallet {}
// Switch expression
String label = switch (status) {
case ACTIVE -> "Active";
case SUSPENDED -> "Suspended";
case CLOSED -> "Closed";
};
```
## Optional Usage
- Return `Optional<T>` from finder methods that may have no result
- Use `map()`, `flatMap()`, `orElseThrow()` — never call `get()` without `isPresent()`
- Never use `Optional` as a field type or method parameter
```java
// GOOD
return repository.findById(id)
.map(ResponseDto::from)
.orElseThrow(() -> new OrderNotFoundException(id));
// BAD — Optional as parameter
public void process(Optional<String> name) {}
```
## Error Handling
- Prefer unchecked exceptions for domain errors
- Create domain-specific exceptions extending `RuntimeException`
- Avoid broad `catch (Exception e)` unless at top-level handlers
- Include context in exception messages
```java
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(Long id) {
super("Order not found: id=" + id);
}
}
```
## Streams
- Use streams for transformations; keep pipelines short (3-4 operations max)
- Prefer method references when readable: `.map(Order::getTotal)`
- Avoid side effects in stream operations
- For complex logic, prefer a loop over a convoluted stream pipeline
## References
See skill: `java-coding-standards` for full coding standards with examples.
See skill: `jpa-patterns` for JPA/Hibernate entity design patterns.
+18
View File
@@ -0,0 +1,18 @@
---
paths:
- "**/*.java"
- "**/pom.xml"
- "**/build.gradle"
- "**/build.gradle.kts"
---
# Java Hooks
> This file extends [common/hooks.md](../common/hooks.md) with Java-specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **google-java-format**: Auto-format `.java` files after edit
- **checkstyle**: Run style checks after editing Java files
- **./mvnw compile** or **./gradlew compileJava**: Verify compilation after changes
+146
View File
@@ -0,0 +1,146 @@
---
paths:
- "**/*.java"
---
# Java Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Java-specific content.
## Repository Pattern
Encapsulate data access behind an interface:
```java
public interface OrderRepository {
Optional<Order> findById(Long id);
List<Order> findAll();
Order save(Order order);
void deleteById(Long id);
}
```
Concrete implementations handle storage details (JPA, JDBC, in-memory for tests).
## Service Layer
Business logic in service classes; keep controllers and repositories thin:
```java
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
public OrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
}
public OrderSummary placeOrder(CreateOrderRequest request) {
var order = Order.from(request);
paymentGateway.charge(order.total());
var saved = orderRepository.save(order);
return OrderSummary.from(saved);
}
}
```
## Constructor Injection
Always use constructor injection — never field injection:
```java
// GOOD — constructor injection (testable, immutable)
public class NotificationService {
private final EmailSender emailSender;
public NotificationService(EmailSender emailSender) {
this.emailSender = emailSender;
}
}
// BAD — field injection (untestable without reflection, requires framework magic)
public class NotificationService {
@Inject // or @Autowired
private EmailSender emailSender;
}
```
## DTO Mapping
Use records for DTOs. Map at service/controller boundaries:
```java
public record OrderResponse(Long id, String customer, BigDecimal total) {
public static OrderResponse from(Order order) {
return new OrderResponse(order.getId(), order.getCustomerName(), order.getTotal());
}
}
```
## Builder Pattern
Use for objects with many optional parameters:
```java
public class SearchCriteria {
private final String query;
private final int page;
private final int size;
private final String sortBy;
private SearchCriteria(Builder builder) {
this.query = builder.query;
this.page = builder.page;
this.size = builder.size;
this.sortBy = builder.sortBy;
}
public static class Builder {
private String query = "";
private int page = 0;
private int size = 20;
private String sortBy = "id";
public Builder query(String query) { this.query = query; return this; }
public Builder page(int page) { this.page = page; return this; }
public Builder size(int size) { this.size = size; return this; }
public Builder sortBy(String sortBy) { this.sortBy = sortBy; return this; }
public SearchCriteria build() { return new SearchCriteria(this); }
}
}
```
## Sealed Types for Domain Models
```java
public sealed interface PaymentResult permits PaymentSuccess, PaymentFailure {
record PaymentSuccess(String transactionId, BigDecimal amount) implements PaymentResult {}
record PaymentFailure(String errorCode, String message) implements PaymentResult {}
}
// Exhaustive handling (Java 21+)
String message = switch (result) {
case PaymentSuccess s -> "Paid: " + s.transactionId();
case PaymentFailure f -> "Failed: " + f.errorCode();
};
```
## API Response Envelope
Consistent API responses:
```java
public record ApiResponse<T>(boolean success, T data, String error) {
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, data, null);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, null, message);
}
}
```
## References
See skill: `springboot-patterns` for Spring Boot architecture patterns.
See skill: `jpa-patterns` for entity design and query optimization.
+100
View File
@@ -0,0 +1,100 @@
---
paths:
- "**/*.java"
---
# Java Security
> This file extends [common/security.md](../common/security.md) with Java-specific content.
## Secrets Management
- Never hardcode API keys, tokens, or credentials in source code
- Use environment variables: `System.getenv("API_KEY")`
- Use a secret manager (Vault, AWS Secrets Manager) for production secrets
- Keep local config files with secrets in `.gitignore`
```java
// BAD
private static final String API_KEY = "sk-abc123...";
// GOOD — environment variable
String apiKey = System.getenv("PAYMENT_API_KEY");
Objects.requireNonNull(apiKey, "PAYMENT_API_KEY must be set");
```
## SQL Injection Prevention
- Always use parameterized queries — never concatenate user input into SQL
- Use `PreparedStatement` or your framework's parameterized query API
- Validate and sanitize any input used in native queries
```java
// BAD — SQL injection via string concatenation
Statement stmt = conn.createStatement();
String sql = "SELECT * FROM orders WHERE name = '" + name + "'";
stmt.executeQuery(sql);
// GOOD — PreparedStatement with parameterized query
PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE name = ?");
ps.setString(1, name);
// GOOD — JDBC template
jdbcTemplate.query("SELECT * FROM orders WHERE name = ?", mapper, name);
```
## Input Validation
- Validate all user input at system boundaries before processing
- Use Bean Validation (`@NotNull`, `@NotBlank`, `@Size`) on DTOs when using a validation framework
- Sanitize file paths and user-provided strings before use
- Reject input that fails validation with clear error messages
```java
// Validate manually in plain Java
public Order createOrder(String customerName, BigDecimal amount) {
if (customerName == null || customerName.isBlank()) {
throw new IllegalArgumentException("Customer name is required");
}
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
return new Order(customerName, amount);
}
```
## Authentication and Authorization
- Never implement custom auth crypto — use established libraries
- Store passwords with bcrypt or Argon2, never MD5/SHA1
- Enforce authorization checks at service boundaries
- Clear sensitive data from logs — never log passwords, tokens, or PII
## Dependency Security
- Run `mvn dependency:tree` or `./gradlew dependencies` to audit transitive dependencies
- Use OWASP Dependency-Check or Snyk to scan for known CVEs
- Keep dependencies updated — set up Dependabot or Renovate
## Error Messages
- Never expose stack traces, internal paths, or SQL errors in API responses
- Map exceptions to safe, generic client messages at handler boundaries
- Log detailed errors server-side; return generic messages to clients
```java
// Log the detail, return a generic message
try {
return orderService.findById(id);
} catch (OrderNotFoundException ex) {
log.warn("Order not found: id={}", id);
return ApiResponse.error("Resource not found"); // generic, no internals
} catch (Exception ex) {
log.error("Unexpected error processing order id={}", id, ex);
return ApiResponse.error("Internal server error"); // never expose ex.getMessage()
}
```
## References
See skill: `springboot-security` for Spring Security authentication and authorization patterns.
See skill: `security-review` for general security checklists.
+131
View File
@@ -0,0 +1,131 @@
---
paths:
- "**/*.java"
---
# Java Testing
> This file extends [common/testing.md](../common/testing.md) with Java-specific content.
## Test Framework
- **JUnit 5** (`@Test`, `@ParameterizedTest`, `@Nested`, `@DisplayName`)
- **AssertJ** for fluent assertions (`assertThat(result).isEqualTo(expected)`)
- **Mockito** for mocking dependencies
- **Testcontainers** for integration tests requiring databases or services
## Test Organization
```
src/test/java/com/example/app/
service/ # Unit tests for service layer
controller/ # Web layer / API tests
repository/ # Data access tests
integration/ # Cross-layer integration tests
```
Mirror the `src/main/java` package structure in `src/test/java`.
## Unit Test Pattern
```java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
private OrderService orderService;
@BeforeEach
void setUp() {
orderService = new OrderService(orderRepository);
}
@Test
@DisplayName("findById returns order when exists")
void findById_existingOrder_returnsOrder() {
var order = new Order(1L, "Alice", BigDecimal.TEN);
when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
var result = orderService.findById(1L);
assertThat(result.customerName()).isEqualTo("Alice");
verify(orderRepository).findById(1L);
}
@Test
@DisplayName("findById throws when order not found")
void findById_missingOrder_throws() {
when(orderRepository.findById(99L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> orderService.findById(99L))
.isInstanceOf(OrderNotFoundException.class)
.hasMessageContaining("99");
}
}
```
## Parameterized Tests
```java
@ParameterizedTest
@CsvSource({
"100.00, 10, 90.00",
"50.00, 0, 50.00",
"200.00, 25, 150.00"
})
@DisplayName("discount applied correctly")
void applyDiscount(BigDecimal price, int pct, BigDecimal expected) {
assertThat(PricingUtils.discount(price, pct)).isEqualByComparingTo(expected);
}
```
## Integration Tests
Use Testcontainers for real database integration:
```java
@Testcontainers
class OrderRepositoryIT {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
private OrderRepository repository;
@BeforeEach
void setUp() {
var dataSource = new PGSimpleDataSource();
dataSource.setUrl(postgres.getJdbcUrl());
dataSource.setUser(postgres.getUsername());
dataSource.setPassword(postgres.getPassword());
repository = new JdbcOrderRepository(dataSource);
}
@Test
void save_and_findById() {
var saved = repository.save(new Order(null, "Bob", BigDecimal.ONE));
var found = repository.findById(saved.getId());
assertThat(found).isPresent();
}
}
```
For Spring Boot integration tests, see skill: `springboot-tdd`.
## Test Naming
Use descriptive names with `@DisplayName`:
- `methodName_scenario_expectedBehavior()` for method names
- `@DisplayName("human-readable description")` for reports
## Coverage
- Target 80%+ line coverage
- Use JaCoCo for coverage reporting
- Focus on service and domain logic — skip trivial getters/config classes
## References
See skill: `springboot-tdd` for Spring Boot TDD patterns with MockMvc and Testcontainers.
See skill: `java-coding-standards` for testing expectations.
+86
View File
@@ -0,0 +1,86 @@
---
paths:
- "**/*.kt"
- "**/*.kts"
---
# Kotlin Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Kotlin-specific content.
## Formatting
- **ktlint** or **Detekt** for style enforcement
- Official Kotlin code style (`kotlin.code.style=official` in `gradle.properties`)
## Immutability
- Prefer `val` over `var` — default to `val` and only use `var` when mutation is required
- Use `data class` for value types; use immutable collections (`List`, `Map`, `Set`) in public APIs
- Copy-on-write for state updates: `state.copy(field = newValue)`
## Naming
Follow Kotlin conventions:
- `camelCase` for functions and properties
- `PascalCase` for classes, interfaces, objects, and type aliases
- `SCREAMING_SNAKE_CASE` for constants (`const val` or `@JvmStatic`)
- Prefix interfaces with behavior, not `I`: `Clickable` not `IClickable`
## Null Safety
- Never use `!!` — prefer `?.`, `?:`, `requireNotNull()`, or `checkNotNull()`
- Use `?.let {}` for scoped null-safe operations
- Return nullable types from functions that can legitimately have no result
```kotlin
// BAD
val name = user!!.name
// GOOD
val name = user?.name ?: "Unknown"
val name = requireNotNull(user) { "User must be set before accessing name" }.name
```
## Sealed Types
Use sealed classes/interfaces to model closed state hierarchies:
```kotlin
sealed interface UiState<out T> {
data object Loading : UiState<Nothing>
data class Success<T>(val data: T) : UiState<T>
data class Error(val message: String) : UiState<Nothing>
}
```
Always use exhaustive `when` with sealed types — no `else` branch.
## Extension Functions
Use extension functions for utility operations, but keep them discoverable:
- Place in a file named after the receiver type (`StringExt.kt`, `FlowExt.kt`)
- Keep scope limited — don't add extensions to `Any` or overly generic types
## Scope Functions
Use the right scope function:
- `let` — null check + transform: `user?.let { greet(it) }`
- `run` — compute a result using receiver: `service.run { fetch(config) }`
- `apply` — configure an object: `builder.apply { timeout = 30 }`
- `also` — side effects: `result.also { log(it) }`
- Avoid deep nesting of scope functions (max 2 levels)
## Error Handling
- Use `Result<T>` or custom sealed types
- Use `runCatching {}` for wrapping throwable code
- Never catch `CancellationException` — always rethrow it
- Avoid `try-catch` for control flow
```kotlin
// BAD — using exceptions for control flow
val user = try { repository.getUser(id) } catch (e: NotFoundException) { null }
// GOOD — nullable return
val user: User? = repository.findUser(id)
```
+17
View File
@@ -0,0 +1,17 @@
---
paths:
- "**/*.kt"
- "**/*.kts"
- "**/build.gradle.kts"
---
# Kotlin Hooks
> This file extends [common/hooks.md](../common/hooks.md) with Kotlin-specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **ktfmt/ktlint**: Auto-format `.kt` and `.kts` files after edit
- **detekt**: Run static analysis after editing Kotlin files
- **./gradlew build**: Verify compilation after changes
+146
View File
@@ -0,0 +1,146 @@
---
paths:
- "**/*.kt"
- "**/*.kts"
---
# Kotlin Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Kotlin and Android/KMP-specific content.
## Dependency Injection
Prefer constructor injection. Use Koin (KMP) or Hilt (Android-only):
```kotlin
// Koin — declare modules
val dataModule = module {
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
factory { GetItemsUseCase(get()) }
viewModelOf(::ItemListViewModel)
}
// Hilt — annotations
@HiltViewModel
class ItemListViewModel @Inject constructor(
private val getItems: GetItemsUseCase
) : ViewModel()
```
## ViewModel Pattern
Single state object, event sink, one-way data flow:
```kotlin
data class ScreenState(
val items: List<Item> = emptyList(),
val isLoading: Boolean = false
)
class ScreenViewModel(private val useCase: GetItemsUseCase) : ViewModel() {
private val _state = MutableStateFlow(ScreenState())
val state = _state.asStateFlow()
fun onEvent(event: ScreenEvent) {
when (event) {
is ScreenEvent.Load -> load()
is ScreenEvent.Delete -> delete(event.id)
}
}
}
```
## Repository Pattern
- `suspend` functions return `Result<T>` or custom error type
- `Flow` for reactive streams
- Coordinate local + remote data sources
```kotlin
interface ItemRepository {
suspend fun getById(id: String): Result<Item>
suspend fun getAll(): Result<List<Item>>
fun observeAll(): Flow<List<Item>>
}
```
## UseCase Pattern
Single responsibility, `operator fun invoke`:
```kotlin
class GetItemUseCase(private val repository: ItemRepository) {
suspend operator fun invoke(id: String): Result<Item> {
return repository.getById(id)
}
}
class GetItemsUseCase(private val repository: ItemRepository) {
suspend operator fun invoke(): Result<List<Item>> {
return repository.getAll()
}
}
```
## expect/actual (KMP)
Use for platform-specific implementations:
```kotlin
// commonMain
expect fun platformName(): String
expect class SecureStorage {
fun save(key: String, value: String)
fun get(key: String): String?
}
// androidMain
actual fun platformName(): String = "Android"
actual class SecureStorage {
actual fun save(key: String, value: String) { /* EncryptedSharedPreferences */ }
actual fun get(key: String): String? = null /* ... */
}
// iosMain
actual fun platformName(): String = "iOS"
actual class SecureStorage {
actual fun save(key: String, value: String) { /* Keychain */ }
actual fun get(key: String): String? = null /* ... */
}
```
## Coroutine Patterns
- Use `viewModelScope` in ViewModels, `coroutineScope` for structured child work
- Use `stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), initialValue)` for StateFlow from cold Flows
- Use `supervisorScope` when child failures should be independent
## Builder Pattern with DSL
```kotlin
class HttpClientConfig {
var baseUrl: String = ""
var timeout: Long = 30_000
private val interceptors = mutableListOf<Interceptor>()
fun interceptor(block: () -> Interceptor) {
interceptors.add(block())
}
}
fun httpClient(block: HttpClientConfig.() -> Unit): HttpClient {
val config = HttpClientConfig().apply(block)
return HttpClient(config)
}
// Usage
val client = httpClient {
baseUrl = "https://api.example.com"
timeout = 15_000
interceptor { AuthInterceptor(tokenProvider) }
}
```
## References
See skill: `kotlin-coroutines-flows` for detailed coroutine patterns.
See skill: `android-clean-architecture` for module and layer patterns.
+82
View File
@@ -0,0 +1,82 @@
---
paths:
- "**/*.kt"
- "**/*.kts"
---
# Kotlin Security
> This file extends [common/security.md](../common/security.md) with Kotlin and Android/KMP-specific content.
## Secrets Management
- Never hardcode API keys, tokens, or credentials in source code
- Use `local.properties` (git-ignored) for local development secrets
- Use `BuildConfig` fields generated from CI secrets for release builds
- Use `EncryptedSharedPreferences` (Android) or Keychain (iOS) for runtime secret storage
```kotlin
// BAD
val apiKey = "sk-abc123..."
// GOOD — from BuildConfig (generated at build time)
val apiKey = BuildConfig.API_KEY
// GOOD — from secure storage at runtime
val token = secureStorage.get("auth_token")
```
## Network Security
- Use HTTPS exclusively — configure `network_security_config.xml` to block cleartext
- Pin certificates for sensitive endpoints using OkHttp `CertificatePinner` or Ktor equivalent
- Set timeouts on all HTTP clients — never leave defaults (which may be infinite)
- Validate and sanitize all server responses before use
```xml
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>
```
## Input Validation
- Validate all user input before processing or sending to API
- Use parameterized queries for Room/SQLDelight — never concatenate user input into SQL
- Sanitize file paths from user input to prevent path traversal
```kotlin
// BAD — SQL injection
@Query("SELECT * FROM items WHERE name = '$input'")
// GOOD — parameterized
@Query("SELECT * FROM items WHERE name = :input")
fun findByName(input: String): List<ItemEntity>
```
## Data Protection
- Use `EncryptedSharedPreferences` for sensitive key-value data on Android
- Use `@Serializable` with explicit field names — don't leak internal property names
- Clear sensitive data from memory when no longer needed
- Use `@Keep` or ProGuard rules for serialized classes to prevent name mangling
## Authentication
- Store tokens in secure storage, not in plain SharedPreferences
- Implement token refresh with proper 401/403 handling
- Clear all auth state on logout (tokens, cached user data, cookies)
- Use biometric authentication (`BiometricPrompt`) for sensitive operations
## ProGuard / R8
- Keep rules for all serialized models (`@Serializable`, Gson, Moshi)
- Keep rules for reflection-based libraries (Koin, Retrofit)
- Test release builds — obfuscation can break serialization silently
## WebView Security
- Disable JavaScript unless explicitly needed: `settings.javaScriptEnabled = false`
- Validate URLs before loading in WebView
- Never expose `@JavascriptInterface` methods that access sensitive data
- Use `WebViewClient.shouldOverrideUrlLoading()` to control navigation
+128
View File
@@ -0,0 +1,128 @@
---
paths:
- "**/*.kt"
- "**/*.kts"
---
# Kotlin Testing
> This file extends [common/testing.md](../common/testing.md) with Kotlin and Android/KMP-specific content.
## Test Framework
- **kotlin.test** for multiplatform (KMP) — `@Test`, `assertEquals`, `assertTrue`
- **JUnit 4/5** for Android-specific tests
- **Turbine** for testing Flows and StateFlow
- **kotlinx-coroutines-test** for coroutine testing (`runTest`, `TestDispatcher`)
## ViewModel Testing with Turbine
```kotlin
@Test
fun `loading state emitted then data`() = runTest {
val repo = FakeItemRepository()
repo.addItem(testItem)
val viewModel = ItemListViewModel(GetItemsUseCase(repo))
viewModel.state.test {
assertEquals(ItemListState(), awaitItem()) // initial state
viewModel.onEvent(ItemListEvent.Load)
assertTrue(awaitItem().isLoading) // loading
assertEquals(listOf(testItem), awaitItem().items) // loaded
}
}
```
## Fakes Over Mocks
Prefer hand-written fakes over mocking frameworks:
```kotlin
class FakeItemRepository : ItemRepository {
private val items = mutableListOf<Item>()
var fetchError: Throwable? = null
override suspend fun getAll(): Result<List<Item>> {
fetchError?.let { return Result.failure(it) }
return Result.success(items.toList())
}
override fun observeAll(): Flow<List<Item>> = flowOf(items.toList())
fun addItem(item: Item) { items.add(item) }
}
```
## Coroutine Testing
```kotlin
@Test
fun `parallel operations complete`() = runTest {
val repo = FakeRepository()
val result = loadDashboard(repo)
advanceUntilIdle()
assertNotNull(result.items)
assertNotNull(result.stats)
}
```
Use `runTest` — it auto-advances virtual time and provides `TestScope`.
## Ktor MockEngine
```kotlin
val mockEngine = MockEngine { request ->
when (request.url.encodedPath) {
"/api/items" -> respond(
content = Json.encodeToString(testItems),
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
)
else -> respondError(HttpStatusCode.NotFound)
}
}
val client = HttpClient(mockEngine) {
install(ContentNegotiation) { json() }
}
```
## Room/SQLDelight Testing
- Room: Use `Room.inMemoryDatabaseBuilder()` for in-memory testing
- SQLDelight: Use `JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)` for JVM tests
```kotlin
@Test
fun `insert and query items`() = runTest {
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
Database.Schema.create(driver)
val db = Database(driver)
db.itemQueries.insert("1", "Sample Item", "description")
val items = db.itemQueries.getAll().executeAsList()
assertEquals(1, items.size)
}
```
## Test Naming
Use backtick-quoted descriptive names:
```kotlin
@Test
fun `search with empty query returns all items`() = runTest { }
@Test
fun `delete item emits updated list without deleted item`() = runTest { }
```
## Test Organization
```
src/
├── commonTest/kotlin/ # Shared tests (ViewModel, UseCase, Repository)
├── androidUnitTest/kotlin/ # Android unit tests (JUnit)
├── androidInstrumentedTest/kotlin/ # Instrumented tests (Room, UI)
└── iosTest/kotlin/ # iOS-specific tests
```
Minimum test coverage: ViewModel + UseCase for every feature.
+616
View File
@@ -0,0 +1,616 @@
---
paths:
- "**/*.lua"
---
# Lua Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Lua specific content.
## Version Compatibility Matrix
These rules target **universal Lua coverage** across 5.0, 5.1, 5.2, 5.3, and 5.4. When a feature differs between versions, use the portable pattern or guard with a version check.
| Feature | 5.0 | 5.1 | 5.2 | 5.3 | 5.4 | Portable Pattern |
|---------|-----|-----|-----|-----|-----|------------------|
| Length operator `#` | ❌ `table.getn` | ✅ | ✅ | ✅ | ✅ | `table.getn(t)` or `#t` with guard |
| Varargs `...` as expr | ❌ `arg` table | ✅ | ✅ | ✅ | ✅ | See Varargs section |
| `select()` | ❌ | ✅ | ✅ | ✅ | ✅ | Guard: `if select then ... else arg[i]` |
| `math.mod` / `math.fmod` | ✅ `math.mod` | ✅ `math.fmod` | ✅ `math.fmod` | ✅ | ✅ | `math.fmod or math.mod` |
| `string.gmatch` | ❌ `string.gfind` | ✅ | ✅ | ✅ | ✅ | `string.gmatch or string.gfind` |
| `setfenv`/`getfenv` | ✅ | ✅ | ❌ `_ENV` | ❌ | ❌ | Version-gated sandbox |
| `unpack()` global | ✅ | ✅ | ❌ `table.unpack` | ❌ | ❌ | `unpack or table.unpack` |
| `xpcall` extra args | ❌ | ❌ | ✅ | ✅ | ✅ | Wrap in closure for 5.0/5.1 |
| `goto` statement | ❌ | ❌ | ✅ | ✅ | ✅ | Avoid; use early return |
| Integer subtype | ❌ | ❌ | ❌ | ✅ | ✅ | All numbers are doubles in 5.05.2 |
| Bitwise operators | ❌ | ❌ | ✅ `bit32` lib | ✅ native (`bit32` deprecated) | ✅ native | `bit32` lib on 5.2; native ops on 5.3+ |
| `__gc` for tables | ❌ | ❌ | ✅ | ✅ | ✅ | Only for userdata in 5.0/5.1 |
| `__len` for tables | ❌ | ❌ (userdata only) | ✅ | ✅ | ✅ | `rawlen` or custom function; 5.1 `__len` only for userdata |
| `table.move` | ❌ | ❌ | ❌ | ✅ | ✅ | Manual loop |
| `table.foreach`/`foreachi` | ✅ | ✅ (deprecated) | ❌ removed | ❌ | ❌ | Use `pairs`/`ipairs` (5.0+) |
| `table.setn` | ✅ | ✅ (deprecated) | ❌ removed | ❌ | ❌ | Track length manually |
| `table.maxn` | ❌ | ✅ | ✅ (deprecated) | ❌ removed | ❌ | Manual loop over keys |
| `package` table | ❌ `loadlib` | ✅ | ✅ | ✅ | ✅ | Guard `package and package.path` |
| `pcall` extra args | ✅ | ✅ | ✅ | ✅ | ✅ | Universal |
| `coroutine.status` | ✅ | ✅ | ✅ | ✅ | ✅ | Universal |
| `table.pack` | ❌ | ❌ | ✅ | ✅ | ✅ | `{n = select("#", ...), ...}` |
| `rawlen()` | ❌ | ❌ | ✅ | ✅ | ✅ | `rawlen or function(t) return #t end` |
| `loadstring` | ✅ | ✅ | ❌ (deprecated→`load`) | ❌ | ❌ | `loadstring or load` |
| `string.pack/unpack` | ❌ | ❌ | ❌ | ✅ | ✅ | External `struct` lib for older versions |
| `utf8` library | ❌ | ❌ | ❌ | ✅ | ✅ | External `lua-utf8` lib for older |
| `package.loaders` | ❌ | ✅ | ❌→`.searchers` | ❌ | ❌ | `package.loaders or package.searchers` |
| `coroutine.isyieldable` | ❌ | ❌ | ❌ | ✅ | ✅ | Guard: `coroutine.isyieldable and ...` |
| `math.atan2` | ✅ | ✅ | ✅ | ✅ | ✅ | `math.atan(y, x)` — note argument order is (y, x), not (x, y) |
| `math.log10` | ❌ | ✅ | ✅ | ❌ removed | ❌ | `math.log(x, 10)` or `math.log(x) / math.log(10)` |
| `math.pow` | ✅ | ✅ | ✅ | ✅ | ✅ | `x ^ y` (native operator) or `math.pow(x, y)` |
| `math.log(x, base)` | ❌ | ❌ | ✅ | ✅ | ✅ | `math.log(x) / math.log(base)` for portable base |
| Floor division `//` | ❌ | ❌ | ❌ | ✅ | ✅ | `math.floor(a / b)` for 5.05.2 |
| `math.cosh/sinh/tanh` | ❌ | ✅ | ✅ | ❌ deprecated | ❌ | Implement manually or use external lib |
| `math.frexp`/`math.ldexp` | ❌ | ✅ | ✅ | ❌ deprecated | ❌ | `x * 2.0^exp` for ldexp; external lib for frexp |
| `coroutine.close` | ❌ | ❌ | ❌ | ❌ | ✅ | Guard: `coroutine.close and coroutine.close(co)` |
| `warn()` function | ❌ | ❌ | ❌ | ❌ | ✅ | Guard: `if warn then warn(msg) end` |
| `<const>`/`<close>` attrs | ❌ | ❌ | ❌ | ❌ | ✅ | Use `local` without attrs on 5.05.3 |
| `__le` metamethod required | ❌ (derived from `__lt`) | ❌ | ❌ | ❌ | ✅ (must define explicitly) | Always define both `__lt` and `__le` |
| String→number coercion | ✅ auto | ✅ auto | ✅ auto | ✅ auto | ❌ removed from core | Use explicit `tonumber()` for portability |
| Long string nesting `[[]]` | ✅ (nestable) | ❌ (no nesting) | ❌ | ❌ | ❌ | Use `[=[...]=]` for nested long strings |
| `%z` pattern class | ✅ | ✅ | ❌ deprecated | ❌ | ❌ | Use literal `\0` to match the null character (ASCII 0) in patterns (5.2+) |
| `__ipairs` metamethod | ❌ | ❌ | ✅ | ❌ deprecated | ❌ | Avoid; `ipairs` uses raw integer keys |
| Float→string `.0` suffix | ❌ `2.0``"2"` | ❌ | ❌ | ✅ `2.0``"2.0"` | ✅ | Use `string.format` for consistent formatting |
| `print` calls `tostring` | ✅ | ✅ | ✅ | ✅ | ❌ (hardwired) | Use `__tostring` metamethod for custom output |
| `math.random` auto-seeded | ❌ | ❌ | ❌ | ❌ | ✅ (new PRNG) | Call `math.randomseed` explicitly on 5.05.3 |
| `math.log(x, base)` | ❌ | ❌ | ✅ | ✅ | ✅ | `math.log(x) / math.log(base)` for base argument |
| Floor division `//` | ❌ | ❌ | ❌ | ✅ | ✅ | `math.floor(a / b)` for 5.05.2 |
| `io.lines` return count | 1 | 1 | 1 | 1 | 4 (line, extra, line number, error) | Wrap in `(io.lines(...))` to get 1 value |
| `collectgarbage("count")` | N/A | 2 values | 2 values | 1 value | 1 value | Use `math.floor(collectgarbage("count"))` |
### Cross-Version Compatibility Shim
Place at the top of your entry point file to normalize APIs across versions:
```lua
-- compat.lua: Universal Lua 5.05.4 shim
-- Place at top of your entry point; all other files use these locals.
-- Core builtins that moved between versions
local unpack = unpack or table.unpack -- 5.2 moved to table
local getn = table.getn or function(t) return #t end -- 5.0 has no #
local setn = table.setn or function() end -- 5.0 tracks length via setn
local maxn = table.maxn or function(t) -- removed in 5.3
local n = 0
for k in pairs(t) do
if type(k) == "number" and k > n then n = k end
end
return n
end
-- String library renames
local gmatch = string.gmatch or string.gfind -- 5.0 uses gfind
-- Math library renames
local fmod = math.fmod or math.mod -- 5.0 uses math.mod
-- Module system: 5.0 has no package table
local loadlib = package and package.loadlib or loadlib
-- Vararg helpers
local getVarargCount = select -- nil in 5.0
and function(...) return select("#", ...) end -- 5.1+: use select
or function() return arg and arg.n or 0 end -- 5.0: use arg.n
-- Code loading: loadstring renamed to load in 5.2
local loadstring = loadstring or load -- 5.2+ removed loadstring
-- Raw operations: rawlen added in 5.2 (bypasses __len)
local rawlen = rawlen or function(v) return #v end -- fallback uses #
-- table.pack: added in 5.2; polyfill for 5.0/5.1
local table_pack = table.pack or function(...)
return { n = select and select("#", ...) or (arg and arg.n or 0), ... }
end
```
## Formatting
- **StyLua** for code formatting — always run `stylua .` before committing
- **Luacheck** for lints — `luacheck . --no-color` (treat warnings as CI failures)
- 4-space indent (StyLua default)
- Max line width: 120 characters
- Trailing commas in multi-line table constructors (prevents noisy diffs)
## Naming
Follow standard Lua conventions:
- `camelCase` for local variables, functions, method names
- `PascalCase` for modules, "classes" (tables used as classes via metatables)
- `UPPER_SNAKE_CASE` for constants and enum-like values
- `_camelCase` (leading underscore) for private/internal helpers
- `UPPER_SNAKE_CASE` for WoW event strings (`"PLAYER_LOGIN"`, `"QUEST_ACCEPTED"`)
```lua
-- GOOD
local QuestieDB = {} -- PascalCase module
local MAX_RETRIES = 3 -- UPPER_SNAKE constant
local questId = 10141 -- camelCase local
local function _countTable(t) -- _camelCase private helper
```
## Scoping: local Is Non-Negotiable
Every variable MUST be `local` unless explicitly required as a global. Globals leak into `_G`, pollute the namespace, introduce untraceable coupling between files, and cause **ADDON_ACTION_BLOCKED** taint in WoW sandboxes.
```lua
-- BAD: Implicit global — leaks into _G, causes taint
myValue = 10
-- GOOD: Explicit local
local myValue = 10
```
**Global caching**: Cache frequently-called global functions into module-level locals. This provides measurable performance improvement in tight loops (Lua resolves locals via stack slot, globals via hash lookup into `_G`).
```lua
-- Cache at file / module top — BEFORE any function definitions
local type = type
local pairs = pairs
local ipairs = ipairs
local next = next
local tostring = tostring
local tinsert = table.insert
local tremove = table.remove
local tconcat = table.concat
local format = string.format
local floor = math.floor
local max = math.max
local min = math.min
local coroutine = coroutine
local setmetatable = setmetatable
local getmetatable = getmetatable
```
## Immutability
> **Language note**: This rule overrides [common/coding-style.md](../common/coding-style.md)'s strict immutability. Lua tables are mutable by design and creating fresh copies on every update is prohibitively expensive for large data sets (e.g., 40,000+ NPC records). Use immutability where practical; use controlled mutation with clear ownership semantics where performance demands it.
When immutability is practical (configuration, options, small payloads):
```lua
-- GOOD — returns a new table with the field updated
local function withField(original, key, value)
local copy = {}
for k, v in pairs(original) do copy[k] = v end
copy[key] = value
return copy
end
```
When mutation is necessary (hot paths, large tables, WoW frame pools), document ownership:
```lua
-- ACCEPTABLE — mutation of owned table, clearly documented
--- Compiles NPC data into the binary cache. Mutates QuestieDB.npcData in place
--- because copying 40k records per compile is prohibitively expensive.
function QuestieDBCompiler:CompileNPCData(sourceData, targetTable)
for id, data in pairs(sourceData) do
targetTable[id] = self:EncodeRow(data)
end
end
```
Use **read-only proxies** for tables that must not be modified after initialization:
```lua
local function readOnly(t)
return setmetatable({}, {
__index = t,
__newindex = function(_, k, _)
error(format("Attempt to modify read-only table at key: %s", tostring(k)), 2)
end,
-- table.getn for 5.0 compat; # for 5.1+
__len = function() return (table.getn or rawlen or function(x) return #x end)(t) end,
})
end
local QUEST_FLAGS = readOnly({ SHARABLE = 0x0008, DAILY = 0x1000 })
```
## Tables
### Construction
Prefer table literals over sequential assignment. The compiler generates fewer instructions and programmer intent is clearer:
```lua
-- GOOD: Single allocation, clear structure
local config = {
maxRetries = 3,
timeout = 5,
version = "1.4.7",
pluginNames = { "WotLKDB", "TurtleDB" },
}
-- BAD: Multiple allocations, fragmented intent
local config = {}
config.maxRetries = 3
config.timeout = 5
config.version = "1.4.7"
config.pluginNames = {}
config.pluginNames[1] = "WotLKDB"
config.pluginNames[2] = "TurtleDB"
```
### Array Building in Loops
Use `t[#t + 1]` (fastest in Lua 5.1+) or `tinsert`. In Lua 5.0, use `tinsert` or track an index manually since `#` does not exist:
```lua
-- GOOD: Fastest array append (Lua 5.1+)
local results = {}
for id, data in pairs(sourceData) do
results[#results + 1] = data
end
-- GOOD: Works on ALL versions (5.05.4)
local results = {}
for id, data in pairs(sourceData) do
tinsert(results, data)
end
-- GOOD: Manual index tracking (universal, fastest in 5.0)
local results = {}
local n = 0
for id, data in pairs(sourceData) do
n = n + 1
results[n] = data
end
```
### Iteration Patterns
| Use case | Pattern | Lua versions | Notes |
|----------|---------|-------------|-------|
| Sequential array (no holes) | `for i = 1, #t do` | 5.1+ | Fastest; no function call overhead |
| Sequential array (universal) | `for i = 1, table.getn(t) do` | 5.0+ | Use `getn` shim for 5.0 compat |
| Array with value | `for i, v in ipairs(t) do` | 5.0+ | Stops at first `nil` hole |
| Dictionary / sparse table | `for k, v in pairs(t) do` | 5.0+ | Unordered; processes every key |
| Universal / taint-free | `for k, v in next, t do` | 5.0+ | Equivalent to `pairs` but avoids metamethod |
| Empty-check | `if next(t) == nil then` | 5.0+ | Only reliable way |
| Counting elements | Custom `_countTable` function | 5.0+ | `#t`/`table.getn` only counts array part |
```lua
-- Empty-check: ONLY correct way
if next(myTable) == nil then
-- Table is truly empty (no array or hash keys)
end
-- WRONG: Unreliable for dictionaries and sparse arrays
if #myTable == 0 then -- BROKEN on { a = 1, b = 2 }
```
### Holes in Arrays
The `#` operator is **undefined** on sparse arrays (arrays with `nil` gaps). If holes are possible, track length with a counter field or use a dedicated array class.
```lua
-- WRONG: Undefined behavior with holes
local t = { 1, nil, 3 }
print(#t) -- Could be 1 or 3 (implementation-dependent)
-- CORRECT: Track length explicitly
local t = { n = 3; 1, nil, 3 }
for i = 1, t.n do print(t[i]) end
```
### Table Recycling and Wipe
Reuse tables to reduce GC pressure in hot paths. Use `wipe()` (WoW) or manual niling:
```lua
-- WoW environment: wipe() clears all keys
wipe(myTable)
-- Standard Lua: manual clear
for k in pairs(myTable) do myTable[k] = nil end
```
## Functions
- Keep functions under 50 lines. Extract helpers prefixed with `_`.
- **Return early** to flatten nesting. Lua has no guard clauses, so explicit early returns are the idiomatic substitute.
- Avoid deep nesting (> 4 levels). Refactor into helper functions.
```lua
-- BAD: Deep nesting
function processQuest(questId)
if questId then
local data = QuestieDB.questData[questId]
if data then
local name = data[1]
if name then
-- 4 levels deep...
end
end
end
end
-- GOOD: Early returns flatten the code
function processQuest(questId)
if not questId then return end
local data = QuestieDB.questData[questId]
if not data then return end
local name = data[1]
if not name then return end
-- Proceed at 1 level of nesting
end
```
### Default Arguments
Lua has no native defaults. Use the `or` idiom for simple cases, explicit `nil` checks for values where `false` or `0` are valid:
```lua
-- Simple default (WRONG if false/0 are valid values)
local function greet(name, greeting)
name = name or "Adventurer"
greeting = greeting or "Hello"
return format("%s, %s!", greeting, name)
end
-- Precise default (handles false/0 correctly)
local function setEnabled(flag)
if flag == nil then flag = true end -- Default to true; false is a valid input
end
```
### Varargs
Varargs behavior differs significantly across Lua versions:
| Version | Access pattern | Length |
|---------|---------------|--------|
| 5.0 | `arg` table (auto-created) | `arg.n` |
| 5.1+ | `...` as expression | `select("#", ...)` |
```lua
-- Lua 5.1+: Use select for length (handles trailing nil)
local function logAll(...)
local n = select("#", ...)
for i = 1, n do
local v = select(i, ...)
print(format("arg[%d] = %s", i, tostring(v)))
end
end
-- Lua 5.0: Use the implicit 'arg' table
local function logAll(...) -- 5.0 creates 'arg' automatically
for i = 1, arg.n do
print(format("arg[%d] = %s", i, tostring(arg[i])))
end
end
-- UNIVERSAL: Works on 5.05.4
local function logAll(...)
local args = select and { n = select("#", ...), ... } or arg
for i = 1, args.n do
print(format("arg[%d] = %s", i, tostring(args[i])))
end
end
```
## Strings
- **Short concatenation**: `..` is fine for 23 pieces.
- **Loop-built strings**: Use `table.concat` to avoid O(n²) intermediate string allocations.
- **Structured output**: Prefer `string.format` over `..` chains for readability and localization.
```lua
-- BAD: O(n²) string growth in a loop
local result = ""
for _, v in ipairs(data) do
result = result .. tostring(v) .. ", "
end
-- GOOD: O(n) via table.concat
local parts = {}
for i, v in ipairs(data) do
parts[i] = tostring(v)
end
local result = tconcat(parts, ", ")
```
## Error Handling
Lua uses `pcall` / `xpcall` as its try/catch equivalent. Use them at system boundaries (event handlers, plugin entry points, data loading). NEVER silently swallow errors.
```lua
-- pcall: Returns ok, result_or_error
local ok, err = pcall(function()
dangerousOperation()
end)
if not ok then
Questie:Debug(Questie.DEBUG_CRITICAL,
"[Module] Operation failed: " .. tostring(err))
end
-- xpcall: Adds a message handler for stack traces
local function errorHandler(msg)
return tostring(msg) .. "\n" .. debugstack(2)
end
local ok, result = xpcall(dangerousOperation, errorHandler)
```
**Version note**: In Lua 5.0 and 5.1, `xpcall` does NOT support passing arguments to the called function. Wrap in a closure:
```lua
-- WRONG in 5.0/5.1: xpcall does not forward args
local ok, result = xpcall(dangerousOp, errorHandler, arg1, arg2)
-- CORRECT (universal): Wrap in closure
local ok, result = xpcall(function()
return dangerousOp(arg1, arg2)
end, errorHandler)
```
### Error Propagation
Lua uses the `success, result` multi-return pattern (no exceptions). Propagate errors by returning `nil, errorMessage`:
```lua
local function loadConfig(path)
local f, err = io.open(path, "r")
if not f then return nil, "Cannot open: " .. tostring(err) end
local content = f:read("*a")
f:close()
if not content or content == "" then
return nil, "Empty config file: " .. path
end
return content
end
-- Caller
local config, err = loadConfig("settings.ini")
if not config then
error("Config load failed: " .. err)
end
```
## Nil Guards and Defensive Access
Always check `nil` before indexing. Chain guards for deep access:
```lua
-- Safe deep access (short-circuit on nil)
local name = myData and myData.info and myData.info.name
-- Explicit nil check when 0, false, or "" are valid values
if myData.count ~= nil then
processCount(myData.count) -- count could be 0
end
```
## Metatables
Use metatables for `__index` delegation, operator overloads, `__tostring`, and read-only enforcement. **Never** expose raw metatables of security-sensitive tables publicly; use `__metatable` to guard them.
```lua
-- Guard metatable from external manipulation
local Secret = {}
Secret.__index = Secret
Secret.__metatable = "Access denied" -- getmetatable() returns this string
function Secret:New(value)
return setmetatable({ _value = value }, Secret)
end
```
### Common Metamethods
| Metamethod | Purpose | Example |
|-----------|---------|---------|
| `__index` | Delegation / inheritance | Class systems, proxy tables |
| `__newindex` | Intercept writes | Read-only guards, validation |
| `__tostring` | Custom `tostring()` output | Debug printing |
| `__call` | Make a table callable | Functor pattern |
| `__len` | Custom `#` operator | Tables: 5.2+ only; userdata: 5.1+; ignored for tables in 5.0/5.1 |
| `__eq`, `__lt`, `__le` | Comparison operators | 5.0+; **5.4**: `__le` must be explicit (no longer derived from `__lt`) |
| `__gc` | Garbage collection finalizer | Tables: 5.2+ only; userdata: 5.0+ |
| `__close` | To-be-closed variable cleanup | 5.4+ only; `local x <close> = resource` |
| `__metatable` | Protect from `getmetatable` | Security-sensitive objects (5.0+) |
| `__concat` | Custom `..` operator | String-like objects (5.0+) |
## Coroutines
Use coroutines for cooperative multitasking — chunked database compilation, lazy iteration over large datasets, and frame-budgeted work in game engines.
**Critical Lua 5.0/5.1 limitation**: You cannot `yield` from inside a `pcall`/`xpcall` call. This throws `"cannot resume dead coroutine"`. Structure your code so the yield happens outside the protected call. This was fixed in Lua 5.2+ (yield-across-pcall support).
**Lua 5.0 note**: The full `coroutine` library (`create`, `resume`, `yield`, `status`, `wrap`) is available in Lua 5.0. However, in 5.0, you cannot yield from inside a C function, a metamethod, or an iterator — only from the main coroutine body. This restriction was relaxed in 5.1 (yield from iterators became possible) and further in 5.2 (yield across `pcall`/`xpcall`).
```lua
local fmod = math.fmod or math.mod -- 5.0 has math.mod, 5.1+ has math.fmod
local co = coroutine.create(function()
for i = 1, 10000 do
processRecord(i)
if fmod(i, 100) == 0 then -- 5.0: no % operator; use fmod shim
coroutine.yield() -- Pause every 100 records
end
end
end)
-- Resume from a frame ticker (WoW: C_Timer.After, OnUpdate)
local function tick()
if coroutine.status(co) ~= "dead" then
local ok, err = coroutine.resume(co)
if not ok then
error("Coroutine error: " .. tostring(err))
end
end
end
```
## Numeric Precision
Lua 5.05.2 use 64-bit IEEE doubles for **all** numbers (no integer subtype). Lua 5.3+ introduced a separate integer subtype. Integers above 2^53 lose precision in double-only versions. Be aware of floating-point edge cases:
```lua
-- WRONG: Float comparison
if result == 0.3 then -- May fail due to IEEE 754
-- CORRECT: Epsilon comparison
local EPSILON = 1e-9
if math.abs(result - 0.3) < EPSILON then
```
## Debug Output
- **No `print()` in production code** — it bypasses logging levels, cannot be filtered, and causes taint in WoW.
- Gate debug output behind a severity level flag.
- Use structured debug helpers.
```lua
-- BAD
print("Loading quest " .. questId)
-- GOOD
Questie:Debug(Questie.DEBUG_DEVELOP,
format("[QuestieDB] Loading quest %d", questId))
```
## File Organization
MANY SMALL FILES > FEW LARGE FILES:
- 200400 lines typical, 800 lines absolute maximum
- One module / class per file
- Group by feature/domain, not by type
- Data files (large lookup tables) may exceed 800 lines — exclude them from line-count rules
```text
Database/
├── QuestieDB.lua # Core query interface
├── compiler.lua # Binary compilation
├── Corrections/
│ ├── QuestCorrections.lua
│ └── NPCCorrections.lua
├── Data/
│ ├── questData.lua # Raw data (exempt from 800-line rule)
│ └── npcData.lua
└── Zones/
└── zoneDB.lua
```
## Code Quality Checklist
Before marking work complete on any Lua file:
- [ ] All variables are `local` (zero undeclared globals)
- [ ] Functions are under 50 lines
- [ ] Files are under 800 lines (data files exempt)
- [ ] No nesting deeper than 4 levels
- [ ] Error paths return `nil, errMsg` or log explicitly — never silently swallowed
- [ ] No hardcoded magic numbers (use named constants)
- [ ] Table iteration uses the correct primitive (`ipairs`, `pairs`, `next`, numeric `for`)
- [ ] String building in loops uses `table.concat`, not `..`
- [ ] `luacheck` passes with zero warnings
- [ ] `stylua --check` passes
+348
View File
@@ -0,0 +1,348 @@
---
paths:
- "**/*.lua"
---
# Lua Hooks
> This file extends [common/hooks.md](../common/hooks.md) with Lua specific content.
## Tool Chain Summary
| Tool | Purpose | Install | Run |
|------|---------|---------|-----|
| **Luacheck** | Static analysis (undefined globals, unused vars, shadowed locals) | `luarocks install luacheck` | `luacheck .` |
| **StyLua** | Opinionated code formatter (Rust-based, fast) | `cargo install stylua` | `stylua .` |
| **Luacov** | Line coverage reporting | `luarocks install luacov` | `busted --coverage && luacov` |
| **lua-language-server** | IDE diagnostics, type checking, completion | VS Code extension | Automatic |
## Static Analysis: Luacheck
Luacheck detects:
- Undefined global variables (critical for preventing taint)
- Unused local variables and function arguments
- Shadowed local variables
- Unreachable code after `return`
- Unused values assigned to variables
### Configuration (`.luacheckrc`)
Place at project root. Be exhaustive with known globals to eliminate false positives.
**Version targeting**: Set `std` based on your runtime. Use `"lua50"` for 5.0, `"lua51"` for 5.1, or `"none"` for maximum strictness (recommended for WoW addons where you must declare every global):
```lua
-- .luacheckrc
-- For Lua 5.0 projects: use "lua50" to allow table.getn, table.setn, etc.
-- For Lua 5.1 projects: use "lua51"
-- For WoW addons: use "none" (strictest; manually declare all globals)
std = "none"
max_line_length = 120
cache = true -- Speed up repeated runs
-- Allowed globals — explicitly list every WoW API function used
globals = {
-- Core addon system
"QuestieLoader", "Questie", "QuestieDB",
}
read_globals = {
-- Lua builtins (read-only)
"select", "unpack", "pcall", "xpcall", "error", "assert",
"type", "tostring", "tonumber", "rawget", "rawset",
"setmetatable", "getmetatable", "next", "pairs", "ipairs",
"coroutine", "string", "table", "math", "bit",
-- WoW Frame API
"CreateFrame", "UIParent",
-- WoW Timer API
"C_Timer",
-- WoW Map API
"C_Map", "C_QuestLog",
-- WoW Unit API
"UnitGUID", "UnitName", "UnitLevel", "UnitFactionGroup",
"UnitClass", "UnitRace", "GetRealmName",
-- WoW Addon API
"IsAddOnLoaded", "GetAddOnInfo", "GetNumAddOns",
"GetAddOnMetadata",
-- WoW Combat API
"InCombatLockdown",
-- WoW Misc
"Enum", "GetTime", "GetLocale", "GetBuildInfo",
"SlashCmdList", "SLASH_QUESTIE1",
"hooksecurefunc", "debugstack", "geterrorhandler",
"print", "format", "wipe", "strsplit", "strtrim",
"tinsert", "tremove",
-- Lua 5.0-specific globals (add if targeting 5.0)
-- "loadlib", -- 5.0 global; moved to package.loadlib in 5.1+
-- Note: table.getn, table.setn, table.foreach, table.foreachi,
-- math.mod, and string.gfind are methods on their parent tables.
-- Luacheck already allows them via the "table", "math", and "string"
-- entries above. Use std = "lua50" if you need full 5.0 stdlib.
-- SavedVariables (read-only access is acceptable)
"QuestieSV",
}
-- Per-directory overrides
files["Database/Data/**"] = {
max_line_length = false, -- Data files have long lines
ignore = { "631" }, -- Allow line length variance
}
files["tests/**"] = {
std = "+busted", -- Add Busted globals (describe, it, assert, etc.)
globals = {
"_G", -- Tests may manipulate _G for mocking
},
}
files["Localization/**"] = {
max_line_length = false, -- Translation strings can be long
}
-- Warnings to suppress project-wide
ignore = {
"212", -- Unused argument (common in callbacks: function(self, event, ...))
"213", -- Unused loop variable (for _ in pairs)
}
```
### Running Luacheck
```bash
# Full project lint
luacheck .
# Single file with column info
luacheck Database/QuestieDB.lua --codes --ranges
# CI mode: no color, non-zero exit on warnings
luacheck . --no-color --formatter plain
# Show only errors (ignore warnings)
luacheck . --only 0
# List all globals used (audit for taint)
luacheck . --globals --no-unused --no-redefined
```
### Common Warning Codes
| Code | Meaning | Fix |
|------|---------|-----|
| 111 | Setting undefined global | Add `local` or add to `globals` list |
| 112 | Mutating undefined global | Same as 111 |
| 113 | Accessing undefined global | Add to `read_globals` or add `local` |
| 211 | Unused local variable | Remove or prefix with `_` |
| 212 | Unused argument | Prefix with `_` or add to `ignore` |
| 311 | Unused value | Remove the assignment |
| 411 | Redefining local variable | Rename or restructure |
| 421 | Shadowing local variable | Rename inner variable |
| 542 | Empty if branch | Add logic or use guard pattern |
## Formatting: StyLua
### Configuration (`stylua.toml`)
```toml
column_width = 120
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 4
quote_style = "AutoPreferDouble"
call_parentheses = "Always"
collapse_simple_statement = "Never"
[sort_requires]
enabled = false # Lua module loading order matters
```
### Commands
```bash
# Check formatting without modifying (CI)
stylua --check .
# Auto-format all Lua files
stylua .
# Format a single file
stylua Database/QuestieDB.lua
# Preview changes (diff mode)
stylua --check --output-format=diff .
```
## Language Server: lua-language-server (Sumneko)
### VS Code Configuration
```json
// .vscode/settings.json
{
// Set to "Lua 5.0" for 5.0 projects, "Lua 5.1" for WoW, etc.
"Lua.runtime.version": "Lua 5.1",
"Lua.diagnostics.globals": [
"Questie", "QuestieLoader", "QuestieDB",
"CreateFrame", "C_Timer", "C_Map", "Enum",
"GetTime", "IsAddOnLoaded", "InCombatLockdown",
"hooksecurefunc", "debugstack", "wipe",
"print", "format", "strsplit"
],
"Lua.workspace.library": [
// Path to WoW API type definitions if available
],
"Lua.workspace.ignoreDir": [
"Database/Data",
".release"
],
"Lua.diagnostics.disable": [
"lowercase-global"
],
"Lua.completion.callSnippet": "Replace",
"Lua.hint.enable": true
}
```
### Type Annotations (EmmyLua / lua-language-server)
Use `---@` annotations to add type safety in supported IDEs:
```lua
---@class QuestieDB
---@field npcData table<number, table>
---@field questData table<number, table>
local QuestieDB = {}
---@param npcId number
---@return table|nil npcData
---@return string|nil errorMessage
function QuestieDB:GetNPC(npcId)
-- ...
end
```
## Pre-Commit Hook
```bash
#!/bin/sh
# .git/hooks/pre-commit
set -e
echo "=== Luacheck ==="
luacheck . --no-color
echo "=== StyLua ==="
stylua --check .
echo "=== All checks passed ==="
```
## Makefile Targets
```makefile
.PHONY: lint format test coverage ci
lint:
luacheck . --no-color
stylua --check .
format:
stylua .
test:
busted --verbose
coverage:
busted --coverage
luacov
@awk '/^Total/ { if ($$4+0 < 80) { print "FAIL: Coverage " $$4 "% < 80%"; exit 1 } else { print "PASS: Coverage " $$4 "%"; } }' luacov.report.out
ci: lint test coverage
```
## CI Pipeline (GitHub Actions)
```yaml
# .github/workflows/lua-ci.yml
name: Lua CI
on: [push, pull_request]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Lua
uses: leafo/gh-actions-lua@v10
with:
luaVersion: "5.1"
- name: Setup LuaRocks
uses: leafo/gh-actions-luarocks@v4
- name: Install Dependencies
run: |
luarocks install luacheck
luarocks install busted
luarocks install luacov
- name: Install StyLua
run: |
curl -L -o stylua.zip https://github.com/JohnnyMorganz/StyLua/releases/latest/download/stylua-linux-x86_64.zip
unzip stylua.zip -d /usr/local/bin/
chmod +x /usr/local/bin/stylua
- name: Lint (Luacheck)
run: luacheck . --no-color
- name: Format Check (StyLua)
run: stylua --check .
- name: Test
run: busted --output=TAP --coverage
- name: Coverage
run: |
luacov
awk '/^Total/ { if ($4+0 < 80) { print "FAIL: " $4 "%"; exit 1 } }' luacov.report.out
```
## PostToolUse Hook Behavior
After every Lua file edit or creation, the agent SHOULD:
1. Run `luacheck <file>` on the modified file
2. Run `stylua --check <file>` on the modified file
3. Report any issues **before** proceeding to the next edit
This catches errors immediately rather than accumulating them across a multi-file change.
## WoW-Specific: Taint Detection
Monitor for these errors in the WoW error log — they indicate your addon is touching protected state:
| Error | Cause | Fix |
|-------|-------|-----|
| `ADDON_ACTION_BLOCKED` | Tainted code called a protected API | Remove the taint source (`loadstring`, `_G` writes) |
| `ADDON_ACTION_FORBIDDEN` | Addon tried to call a hardware event API | Guard with `InCombatLockdown()` |
| `Couldn't find frame` | Invalid secure template reference | Check template names in `CreateFrame` |
Common taint sources and their fixes:
| Taint Source | Fix |
|-------------|-----|
| `loadstring()` in addon code | Replace with function dispatch tables |
| `_G.MyAddon_Data = data` | Use `addonTable` from the TOC vararg |
| Writing globals from `OnUpdate` | Move writes to `ADDON_LOADED` or `PLAYER_LOGIN` |
| `rawset(_G, name, value)` | Use local module tables instead |
| Hooking with function replacement | Use `hooksecurefunc()` (post-hook only, never overwrite) |
| Calling restricted APIs after taint | Isolate tainted code from secure code paths |
+484
View File
@@ -0,0 +1,484 @@
---
paths:
- "**/*.lua"
---
# Lua Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Lua specific content.
## Module Pattern
The standard Lua module returns a table as its public API. All internal state and helpers are file-local. This is the foundation of all Lua architecture.
```lua
-- mymodule.lua
local M = {}
-- Private state — invisible outside this file
local _cache = {}
local _initialized = false
-- Private helper — underscore prefix signals internal use
local function _buildCacheKey(id)
return "key_" .. tostring(id)
end
-- Public API — the only things callers can access
function M.get(id)
return _cache[_buildCacheKey(id)]
end
function M.init()
if _initialized then return end
_initialized = true
-- one-time setup
end
return M
```
### Legacy Module Styles (5.0 and 5.1)
In Lua 5.0, `setfenv` was used directly to create module environments (there was no `module()` function). In Lua 5.1, the `module()` built-in was introduced. Both are deprecated/removed in 5.2+:
```lua
-- LEGACY 5.0 style — setfenv only (no module() function exists in 5.0)
local M = {}
setfenv(1, M) -- Sets the current function's environment to M
function get(id) -- Automatically scoped to M
return cache[id]
end
return M
-- LEGACY 5.1 style — module() function (introduced in 5.1, deprecated in 5.2)
module("mymodule") -- Creates a global table, sets the function env
function get(id) -- Automatically added to the module table
return cache[id]
end
-- MODERN style (5.0+) — return-table pattern works on ALL versions
local M = {}
function M.get(id) return cache[id] end
return M
```
**Rule**: Always use the return-table pattern. It works on Lua 5.05.4, does not depend on `setfenv` or `module()`, and does not pollute the global namespace.
## Loader / ImportModule Pattern
In large addon systems, a central loader avoids circular `require()` chains by acting as a module registry with lazy resolution:
```lua
-- QuestieLoader.lua
local Loader = {}
local _modules = {}
function Loader:CreateModule(name)
local mod = {}
_modules[name] = mod
return mod
end
function Loader:ImportModule(name)
return _modules[name]
end
-- Usage in a module file:
local QuestieDB = QuestieLoader:CreateModule("QuestieDB")
-- Usage as a consumer:
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
```
This pattern breaks circular dependencies because modules register themselves at parse time but only call `ImportModule` at runtime (inside function bodies).
## OOP / "Class" Pattern
Simulate classes and inheritance with metatables. This is the most common OOP approach in Lua.
### Basic Class
```lua
local Animal = {}
Animal.__index = Animal
function Animal:New(name, sound)
return setmetatable({
name = name,
sound = sound,
}, self) -- 'self' is Animal here (or a subclass)
end
function Animal:Speak()
return self.name .. " says " .. self.sound
end
```
### Inheritance
Chain `__index` through the class hierarchy. Call parent constructors with dot-notation (not colon) to avoid double-wrapping `self`:
```lua
-- Dog extends Animal
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog
function Dog:New(name)
-- CORRECT: dot-notation passes 'self' (Dog) explicitly
local instance = Animal.New(self, name, "Woof")
return setmetatable(instance, Dog)
end
function Dog:Fetch(item)
return self.name .. " fetches the " .. item
end
-- BAD: colon-notation would pass Dog as 'self' twice
-- local instance = Animal:New(name, "Woof") -- WRONG
```
### Mixins
Add behavior from multiple sources without full multiple inheritance:
```lua
local Serializable = {}
function Serializable:Serialize()
local parts = {}
for k, v in pairs(self) do
table.insert(parts, tostring(k) .. "=" .. tostring(v))
end
return table.concat(parts, ";")
end
-- Mixin application
local function applyMixin(class, mixin)
for k, v in pairs(mixin) do
if class[k] == nil then -- Don't overwrite existing methods
class[k] = v
end
end
end
applyMixin(Dog, Serializable)
-- Now Dog instances have :Serialize()
```
## Singleton Pattern
Wrap initialization in a one-time guard. Common for managers, registries, and services:
```lua
local ConfigManager = {}
local _config = nil
function ConfigManager:Get(key)
if not _config then
_config = self:_load()
end
return _config[key]
end
function ConfigManager:_load()
-- expensive one-time load
return { debug = false, maxLevel = 80 }
end
```
## Registry / Plugin Architecture
Use a central registry for runtime plugin discovery without hard coupling. This is the pattern used by `QuestiePluginAPI`:
```lua
local PluginRegistry = { _plugins = {} }
function PluginRegistry:Register(name, pluginData)
assert(type(name) == "string", "Plugin name must be a string")
assert(not self._plugins[name], "Plugin already registered: " .. name)
local plugin = {
name = name,
data = pluginData or {},
stats = { QUEST = 0, NPC = 0, OBJECT = 0, ITEM = 0 },
}
self._plugins[name] = plugin
return plugin
end
function PluginRegistry:Get(name)
return self._plugins[name]
end
function PluginRegistry:IsAnyLoaded()
return next(self._plugins) ~= nil
end
```
### Plugin-Side Registration
Plugins register at parse time (top of file, outside any event handler) so the core can discover them immediately:
```lua
-- In plugin Loader.lua (top level, not inside PLAYER_LOGIN)
local plugin = PluginRegistry:Register("WotLKDB", addonTable)
```
## Observer / Event Bus Pattern
Decouple producers from consumers. Essential for addon systems where modules load in unpredictable order:
```lua
local EventBus = { _listeners = {} }
function EventBus:On(event, fn)
self._listeners[event] = self._listeners[event] or {}
-- Use table.insert for 5.0 compat (no # operator)
table.insert(self._listeners[event], fn)
end
function EventBus:Off(event, fn)
local listeners = self._listeners[event]
if not listeners then return end
-- Reverse iterate; table.getn for 5.0, # for 5.1+
local n = table.getn and table.getn(listeners) or #listeners
for i = n, 1, -1 do
if listeners[i] == fn then
table.remove(listeners, i)
return
end
end
end
function EventBus:Emit(event, ...)
local listeners = self._listeners[event]
if not listeners then return end
-- ipairs works on all versions (5.0+)
for _, fn in ipairs(listeners) do
fn(...) -- 5.1+: ... is an expression; 5.0: use unpack(arg) instead
end
end
```
## Coroutine-Based Lazy Iterator
Turn expensive database scans into resumable, lazy iterations without building the full result set in memory:
```lua
local function filteredNPCs(npcData, predicate)
return coroutine.wrap(function()
for id, data in pairs(npcData) do
if data and predicate(id, data) then
coroutine.yield(id, data)
end
end
end)
end
-- Usage: processes NPCs one at a time, no intermediate table
for npcId, npcData in filteredNPCs(QuestieDB.npcData, function(id, d)
return d[2] and d[2] >= 70 -- level >= 70
end) do
processNPC(npcId, npcData)
end
```
**Version note**: `coroutine.wrap` is available in Lua 5.0+. However, in 5.0 you cannot yield from inside an iterator used in a generic `for` loop as part of a C boundary. The wrap-based pattern above works because the `for` loop drives `resume` directly.
## Chunked Processing (Frame-Budgeted Work)
For operations that must not freeze the game client, split work across frames:
```lua
local function createChunkedProcessor(items, processFunc, chunkSize)
chunkSize = chunkSize or 100
local keys = {}
-- Use table.insert for 5.0 compat
for k in pairs(items) do table.insert(keys, k) end
local index = 1
local total = table.getn and table.getn(keys) or #keys
return function() -- Call this each frame/tick
local budget = math.min(index + chunkSize - 1, total)
for i = index, budget do
processFunc(keys[i], items[keys[i]])
end
index = budget + 1
return index > total -- Returns true when done
end
end
-- Usage with WoW C_Timer
local processor = createChunkedProcessor(rawData, compileRecord, 200)
local ticker
ticker = C_Timer.NewTicker(0.01, function()
if processor() then
ticker:Cancel()
print("Compilation complete!")
end
end)
```
## Memoization / Caching
Cache expensive computations. Support cache invalidation:
```lua
local _zoneCache = {}
local _zoneCacheDirty = false
function ZoneDB:GetZoneAreaId(uiMapId)
if not _zoneCacheDirty and _zoneCache[uiMapId] ~= nil then
return _zoneCache[uiMapId]
end
local result = self:_computeZoneAreaId(uiMapId)
_zoneCache[uiMapId] = result
return result
end
function ZoneDB:InvalidateCache()
wipe(_zoneCache)
_zoneCacheDirty = false
end
```
**Caution**: Use `~= nil` for the cache check, not truthiness. Cached `false` or `0` values are valid and must not trigger recomputation.
## AddonTable Pattern (WoW-Specific)
The WoW client passes a private shared table to every file listed in the addon's `.toc`:
```lua
-- File 1: data.lua
local addonName, addonTable = ...
addonTable.npcData = { [1] = { "Ragnaros", 63, 1 } }
-- File 2: init.lua — same addonTable reference
local addonName, addonTable = ...
local npc = addonTable.npcData[1]
print(npc[1]) -- "Ragnaros"
```
**Critical rule**: Use `addonTable` as the **sole** inter-file communication channel. Never use `_G` for data sharing between files — it pollutes the namespace, causes taint, and is visible to every addon in the client.
**Lua 5.0 note**: The vararg `...` syntax to capture `addonName, addonTable` works differently in 5.0. In 5.0, you must use the implicit `arg` table:
```lua
-- Lua 5.1+:
local addonName, addonTable = ...
-- Lua 5.0:
local addonName = arg[1]
local addonTable = arg[2]
-- Universal (works on 5.05.4):
local addonName = select and select(1, ...) or (arg and arg[1])
local addonTable = select and select(2, ...) or (arg and arg[2])
```
## Proxy / Facade Pattern
Wrap a complex subsystem behind a simplified interface:
```lua
local QuestieAPI = {}
function QuestieAPI:GetQuestName(questId)
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
local data = QuestieDB and QuestieDB:GetQuest(questId)
return data and data.name or "Unknown Quest"
end
```
## Functional Patterns
Lua supports higher-order functions. Use them for data transformations:
```lua
-- map: Transform each element (uses ipairs for 5.0 compat)
local function map(t, fn)
local result = {}
for i, v in ipairs(t) do result[i] = fn(v, i) end
return result
end
-- filter: Keep elements matching predicate
local function filter(t, predicate)
local result = {}
for i, v in ipairs(t) do
if predicate(v, i) then table.insert(result, v) end
end
return result
end
-- reduce: Fold elements into a single value
local function reduce(t, fn, initial)
local acc = initial
for i, v in ipairs(t) do acc = fn(acc, v, i) end
return acc
end
-- Compose: Right-to-left function composition
-- Note: Uses arg.n in 5.0 since select() doesn't exist
local function compose(...)
local fns = select and { ... } or arg
local n = select and select("#", ...) or fns.n
return function(x)
for i = n, 1, -1 do x = fns[i](x) end
return x
end
end
```
## Enumerations
Lua has no native enums. Simulate with constant tables. Optionally freeze with `readOnly()` (see coding-style.md):
```lua
local DebugLevel = {
CRITICAL = 1,
INFO = 2,
DEVELOP = 3,
}
local QuestFlags = {
SHARABLE = 0x0008,
DAILY = 0x1000,
WEEKLY = 0x8000,
AUTO_ACCEPT = 0x80000,
}
```
## Weak Tables
Use weak references for caches that should not prevent garbage collection:
```lua
-- Values are weak: GC can collect them when no other references exist
local textureCache = setmetatable({}, { __mode = "v" })
function getTexture(path)
local cached = textureCache[path]
if cached then return cached end
local tex = loadTexture(path)
textureCache[path] = tex
return tex
end
```
| Mode | Meaning |
|------|---------|
| `__mode = "v"` | Weak values — GC collects values with no other refs |
| `__mode = "k"` | Weak keys — GC collects keys with no other refs |
| `__mode = "kv"` | Both weak — GC collects either direction |
**Ephemeron tables (5.2+)**: In Lua 5.2, weak tables with weak keys behave as **ephemeron tables**. In an ephemeron table, a value is considered reachable only if its key is reachable. If the only reference to a key comes through its value (e.g., a table used as both key and value), the entry is removed. This prevents reference cycles from keeping entries alive and is the correct behavior for cache patterns. In 5.0/5.1, a strong value could keep a weak-keyed entry alive even if the key was unreachable.
**Mode reference**:
| Mode | When entry is removed |
|------|----------------------|
| `"v"` | Value is garbage-collectable and no other references exist |
| `"k"` | Key is garbage-collectable and no other references exist |
| `"kv"` | Either key or value is independently collectable |
+479
View File
@@ -0,0 +1,479 @@
---
paths:
- "**/*.lua"
---
# Lua Security
> This file extends [common/security.md](../common/security.md) with Lua specific content.
## Mandatory Security Checks (Lua)
Before ANY commit of Lua code:
- [ ] No hardcoded secrets (API keys, passwords, tokens, webhook URLs)
- [ ] All user/external inputs validated before processing
- [ ] No `loadstring` / `load` with untrusted input
- [ ] No `os.execute` / `io.popen` with user-controlled strings
- [ ] No unintentional global variables (verified by `luacheck` with `std = "none"`)
- [ ] Error messages don't leak file paths, stack traces, or internal state
- [ ] SavedVariables don't store sensitive data in plaintext
- [ ] Plugin sandbox restricts access to dangerous libraries
## Secrets Management
- NEVER hardcode API keys, tokens, or credentials in Lua source files
- Use environment variables (`os.getenv("API_KEY")`) for CLI/server Lua
- Use secure SavedVariables with obfuscation for addon credentials (if absolutely necessary)
- Fail fast if required secrets are missing at startup
- Keep `.env` files and SavedVariables files in `.gitignore`
```lua
-- BAD: Hardcoded secret
local API_KEY = "sk-abc123secretkey"
-- GOOD: Environment variable with early validation
local function loadApiKey()
local key = os.getenv("API_KEY")
if not key or key == "" then
error("API_KEY environment variable must be set")
end
return key
end
```
## `loadstring` and Dynamic Code Execution
`loadstring` (Lua 5.0/5.1) / `load` (Lua 5.2+) execute arbitrary strings as code. This is the **single most critical attack surface** in Lua.
**Version note**: `loadstring` exists in Lua 5.0 and 5.1. In Lua 5.2+, `loadstring` was removed and its functionality was merged into `load`. In Lua 5.0, `loadlib` (not `loadstring`) is also available for loading C libraries — it was moved to `package.loadlib` in 5.1 and later.
### Never Do This
```lua
-- CRITICAL RISK: Arbitrary code execution from user input
local fn = loadstring(userInput)
if fn then fn() end
-- CRITICAL RISK: Loading from untrusted file path
local fn = loadfile(userSuppliedPath)
if fn then fn() end
-- CRITICAL RISK: Dynamic code from network data
local fn = loadstring(httpResponse.body)
```
### When It Is Acceptable
- Deserializing data from a **trusted, internal source** (e.g., `AceSerializer` output from your own SavedVariables written by your own addon)
- Compile-time / build tooling code that never runs in production
- MUST have a `-- SECURITY: loadstring used here because ...` comment
### Alternatives to loadstring
| Problem | Use Instead |
|---------|-------------|
| Dynamic function dispatch | Function lookup tables |
| Computed field access | `rawget(table, key)` |
| Data deserialization | `AceSerializer`, JSON parser, custom binary format |
| Template expansion | `string.format` or `gsub` with controlled patterns |
```lua
-- BAD: Dynamic dispatch via loadstring
local fn = loadstring("return " .. actionName .. "()")
-- GOOD: Function dispatch table
local actions = {
attack = function() return doAttack() end,
defend = function() return doDefend() end,
heal = function() return doHeal() end,
}
local fn = actions[actionName]
if fn then fn() end
```
### Bytecode Loading (5.2+ Security Risk)
Starting in Lua 5.2, bytecode verification was removed. Loading untrusted binary data via `load()` or `loadfile()` can execute arbitrary code even without `loadstring`. **Always restrict to text-only mode when loading untrusted input:**
```lua
-- CRITICAL: restrict to text mode when source is untrusted
-- The 4th arg 't' = text only, 'b' = binary only, 'bt' = both (default, unsafe)
-- In 5.2: mode is 3rd arg; in 5.3+: mode is 4th arg (after chunk name)
local fn, err = load(untrusted_source, "=(untrusted)", nil, "t")
-- Safe alternative: explicit source validation before loading
if not isKnownTrustedSource(source) then
return nil, "Refused to load untrusted source"
end
-- In Lua 5.1 and earlier, load() does not accept a mode parameter.
-- loadstring() only accepts text, so binary injection is not a risk via loadstring.
-- However, loadfile() can load binary chunks in all versions — validate files first.
```
*Source: Lua 5.2 Manual §8.2 — "Lua does not have bytecode verification anymore. So, all functions that load code (load and loadfile) are potentially insecure when loading untrusted binary data."*
## Global Namespace Leakage
Every variable written without `local` is an implicit global in Lua. This has severe consequences:
1. **Cross-addon contamination**: Any addon can read or overwrite your globals
2. **Information leak**: Internal data structures become publicly visible
3. **Taint**: In WoW, global writes from a tainted call stack propagate taint to the written variable, which then propagates to anything that reads it
4. **Silent bugs**: Typos in variable names silently create new globals instead of erroring
### Prevention
```lua
-- Use luacheck with std = "none" to catch ALL undeclared globals
-- See hooks.md for .luacheckrc configuration
-- BAD: Leaks NPC data into the global namespace
QuestieX_WotLKDB_npc = addonTable.npcData -- Visible to every addon!
-- GOOD: Share via Plugin API (private channel)
local plugin = QuestiePluginAPI:RegisterPlugin("WotLKDB")
plugin.data = addonTable -- Only accessible through the registry
```
### Runtime Global Access Monitoring (Development)
For debugging, use a `__newindex` hook on `_G` to detect unexpected global writes:
```lua
-- WARNING: Development only — remove before release
if DEBUG_MODE then
setmetatable(_G, {
__newindex = function(t, k, v)
local info = debug.getinfo(2, "Sl")
print(string.format(
"WARNING: Global write: %s = %s at %s:%d",
tostring(k), tostring(v),
info.short_src, info.currentline
))
rawset(t, k, v)
end
})
end
```
## Input Validation at System Boundaries
Validate all external input — user text, network data, saved variable files, addon communication — before processing:
```lua
-- GOOD: Validate before use, return nil + error for invalid input
local function safeGetNPC(npcId)
if type(npcId) ~= "number" then
return nil, "npcId must be a number, got: " .. type(npcId)
end
if npcId <= 0 or npcId ~= math.floor(npcId) then
return nil, "npcId must be a positive integer, got: " .. tostring(npcId)
end
return QuestieDB:GetNPC(npcId)
end
-- GOOD: Validate deserialized data structure
local function validateConfig(config)
if type(config) ~= "table" then return nil, "config must be a table" end
if type(config.version) ~= "string" then return nil, "config.version must be a string" end
if type(config.maxLevel) ~= "number" then return nil, "config.maxLevel must be a number" end
if config.maxLevel < 1 or config.maxLevel > 100 then
return nil, "config.maxLevel out of range: " .. config.maxLevel
end
return config
end
```
### SavedVariables Validation
Always validate SavedVariables on load — they can be manually edited by users or corrupted:
```lua
function Questie:LoadSavedVariables()
local sv = QuestieSV
if type(sv) ~= "table" then
-- Corrupted or missing — reset to defaults
QuestieSV = self:GetDefaults()
return
end
-- Validate schema version
if type(sv.version) ~= "number" or sv.version < MIN_SV_VERSION then
-- Schema too old — migrate or reset
QuestieSV = self:MigrateSV(sv)
end
end
```
## File I/O (Standalone Lua)
In sandboxed environments (WoW), `io` is not available. In standalone scripts:
### Path Traversal Prevention
```lua
-- BAD: User-controlled path — allows directory traversal
local f = io.open(userPath, "r")
-- GOOD: Validate path is within allowed directory
local function safeOpen(filename, mode)
-- Strip path traversal attempts
if filename:find("%.%.") or filename:find("[/\\]") then
return nil, "Invalid filename: path traversal detected"
end
local fullPath = SAFE_DIRECTORY .. "/" .. filename
return io.open(fullPath, mode)
end
```
### Resource Management
Always close file handles to prevent resource exhaustion:
```lua
-- BAD: Handle leak on error
local f = io.open("data.txt", "r")
local content = f:read("*a") -- If this errors, f is never closed
f:close()
-- GOOD: Protected read with guaranteed close
local function readFile(path)
local f, err = io.open(path, "r")
if not f then return nil, err end
local ok, content = pcall(f.read, f, "*a")
f:close() -- Always close, even if read failed
if not ok then return nil, content end
return content
end
```
## Shell Injection (`os.execute` / `io.popen`)
NEVER pass user-controlled strings to shell commands:
```lua
-- CRITICAL RISK: Shell injection
os.execute("grep " .. userInput .. " /var/log/app.log")
-- An attacker sends: "; rm -rf / #" as userInput
-- CRITICAL RISK: Same with io.popen
local handle = io.popen("curl " .. userUrl)
-- SAFE: Use validated, sanitized inputs or avoid shell entirely
local function safeLookup(word)
-- Validate: alphanumeric only
if not word:match("^%w+$") then
return nil, "Invalid input: must be alphanumeric"
end
-- Now safe to use in a controlled command
return os.execute("grep -w " .. word .. " dictionary.txt")
end
```
## Sandbox Design for Plugin Systems
When building systems that run third-party plugin code, restrict the execution environment:
### Environment Restriction
```lua
-- Create a restricted environment for plugin execution
local function createSandbox()
return {
-- Safe builtins
print = print,
pairs = pairs,
ipairs = ipairs,
next = next,
type = type,
tostring = tostring,
tonumber = tonumber,
select = select,
unpack = unpack,
error = error,
pcall = pcall,
-- Safe libraries (read-only subsets)
string = {
format = string.format,
find = string.find,
sub = string.sub,
len = string.len,
lower = string.lower,
upper = string.upper,
},
table = {
insert = table.insert,
remove = table.remove,
sort = table.sort,
concat = table.concat,
},
math = {
floor = math.floor,
ceil = math.ceil,
min = math.min,
max = math.max,
abs = math.abs,
},
-- EXPLICITLY EXCLUDED:
-- os (shell access, file system)
-- io (file system access)
-- debug (CRITICAL: debug.getupvalue/setupvalue can read/modify
-- any upvalue in any function, bypassing sandbox entirely.
-- debug.getlocal/setlocal can read/modify locals on the
-- call stack. debug.setmetatable bypasses __metatable guards.
-- Never expose ANY debug library function to untrusted code.)
-- load (arbitrary code execution)
-- loadstring (arbitrary code execution)
-- loadfile (arbitrary code execution)
-- dofile (arbitrary code execution)
-- rawget (bypass metamethod guards)
-- rawset (bypass metamethod guards)
-- setmetatable (override protections)
-- getmetatable (inspect protected tables)
}
end
-- Execute plugin code in sandbox
-- Version-gated: setfenv for 5.0/5.1, _ENV wrapper for 5.2+
local function runInSandbox(code, sandbox)
if setfenv then
-- Lua 5.0 / 5.1: setfenv directly restricts the environment
local fn, err = loadstring(code)
if not fn then return nil, "Compile error: " .. err end
setfenv(fn, sandbox)
return pcall(fn)
else
-- Lua 5.2+: use load() with custom _ENV
-- The 4th arg to load() sets the environment
local fn, err = load(code, "=(sandbox)", "t", sandbox)
if not fn then return nil, "Compile error: " .. err end
return pcall(fn)
end
end
```
**Why this matters**: `setfenv`/`getfenv` were removed in Lua 5.2. Code that relies on `setfenv` for sandboxing will silently fail or error on 5.2+. Always use the version-gated pattern above.
### Resource Limiting
For untrusted code, add execution timeout via `debug.sethook` (available in Lua 5.0+):
```lua
local function runWithTimeout(fn, maxInstructions)
maxInstructions = maxInstructions or 1000000
local count = 0
debug.sethook(function()
count = count + 1
if count > maxInstructions then
error("Execution limit exceeded: suspected infinite loop")
end
end, "", 1) -- Hook every instruction
local ok, result = pcall(fn)
debug.sethook() -- Remove hook
return ok, result
end
```
**Version note**: `debug.sethook` is available in Lua 5.0+ and works identically across all versions. The `debug` library itself may be stripped in sandboxed environments (WoW does not expose the full `debug` library to addons).
## Dependency Security
- Audit third-party Lua libraries (LuaRocks packages) for known CVEs before vendoring
- Pin dependency versions in rockspec or lockfile — never use floating `latest`
- Prefer small, auditable libraries over large frameworks for security-sensitive code
- Vendor critical dependencies (copy into project) rather than relying on external resolution
- Review transitive dependencies: `luarocks show <package>` lists deps
```bash
# List all installed packages and versions
luarocks list
# Show package info including dependencies
luarocks show lpeg
# Install specific version (avoid floating latest)
luarocks install luacheck 1.1.2
```
## Error Message Security
Never expose internal details in user-facing error messages:
```lua
-- BAD: Leaks internal path and database schema
error("Failed to load NPC " .. npcId .. " from "
.. dbPath .. ": column 'rawdata' is nil at index " .. idx)
-- GOOD: Generic user message, detailed internal log
Questie:Debug(Questie.DEBUG_CRITICAL,
format("[QuestieDB] GetNPC failed: npcId=%d, rawdata=nil, source=%s",
npcId, dbPath))
return nil -- Return nil to caller, no internal details
```
## WoW-Specific: Taint and Secure Code
Taint is a security mechanism in the WoW client that prevents addon code from executing protected actions (opening bags during combat, using abilities, etc.). Understanding taint is CRITICAL for WoW addon development.
### Taint Propagation Rules
1. Any variable written from insecure (addon) code is **tainted**
2. Any variable read from tainted state becomes **tainted**
3. Taint propagates through function calls, table reads, and variable assignments
4. Protected API calls from a tainted call stack trigger `ADDON_ACTION_BLOCKED`
### Common Taint Sources and Fixes
| Taint Source | Why It Taints | Fix |
|-------------|---------------|-----|
| `_G.MyVar = value` | Global write from addon code | Use `addonTable` instead |
| `loadstring(code)()` | Compiled code is always tainted | Use function dispatch tables |
| Overwriting Blizzard functions | Replaces secure with insecure | Use `hooksecurefunc()` (post-hook) |
| Writing from `OnUpdate` | Frequent tainted writes | Move to `ADDON_LOADED` or `PLAYER_LOGIN` |
| `rawset(_G, k, v)` | Bypasses metamethods but still taints | Avoid; use local module tables |
| Reading a tainted global | Taint propagates to reader | Read during `ADDON_LOADED` or cache locally |
### Safe WoW API Patterns
```lua
-- GOOD: Post-hook (does not replace the original, does not taint)
hooksecurefunc("QuestLogFrame_Update", function()
-- Your code runs AFTER the original — cannot taint it
end)
-- BAD: Function replacement (replaces secure with insecure = taint)
local original = QuestLogFrame_Update
QuestLogFrame_Update = function(...) -- Now tainted!
original(...)
myCustomLogic()
end
-- GOOD: Combat guard for protected actions
local function safeAction()
if InCombatLockdown() then
-- Queue for after combat
return
end
-- Safe to call protected APIs
end
```
## Security Response Protocol
If a security issue is found in Lua code:
1. **STOP** immediately — do not ship the code
2. Use **security-reviewer** agent
3. Fix CRITICAL issues before continuing
4. If secrets were exposed, rotate them immediately
5. Audit the entire codebase for similar patterns
6. Add `luacheck` rules to prevent recurrence
7. Add regression tests for the specific vulnerability
## References
See skill: `security-review` for general security checklists applicable across all languages.
+397
View File
@@ -0,0 +1,397 @@
---
paths:
- "**/tests/**/*.lua"
- "**/*_spec.lua"
- "**/*_test.lua"
---
# Lua Testing
> This file extends [common/testing.md](../common/testing.md) with Lua specific content.
## Test Frameworks
| Framework | Best for | Install |
|-----------|---------|---------|
| **Busted** | BDD-style unit/integration testing (most popular) | `luarocks install busted` |
| **LuaUnit** | xUnit-style, zero dependencies, minimal footprint | `luarocks install luaunit` |
| **Telescope** | Flexible, extensible, custom reporters | `luarocks install telescope` |
| **WoW mocks** | Game addon testing with C_API stubs | Custom `tests/mocks/` |
**Default choice**: Use **Busted** unless the project has an existing convention.
## Test Organization
```text
project/
├── src/ # Production code
│ ├── Database/
│ │ └── QuestieDB.lua
│ └── Modules/
│ └── QuestieInit.lua
├── tests/
│ ├── mocks/ # Environment stubs
│ │ ├── wow_api.lua # CreateFrame, C_Timer, GetTime stubs
│ │ ├── questie_env.lua # QuestieLoader, Questie object stubs
│ │ └── saved_vars.lua # SavedVariables mock
│ ├── unit/ # Unit tests (one per module)
│ │ ├── QuestieDB_spec.lua
│ │ ├── ZoneDB_spec.lua
│ │ └── compiler_spec.lua
│ ├── integration/ # Multi-module interaction tests
│ │ └── init_flow_spec.lua
│ └── helpers/ # Shared test utilities
│ ├── assertions.lua # Custom assert functions
│ └── fixtures.lua # Reusable test data
├── .busted # Busted configuration
└── .luacov # Coverage configuration
```
- Name spec files with `_spec.lua` suffix (Busted convention) or `_test.lua`
- Mirror the source directory structure in `tests/unit/`
- One spec file per production module
## Busted Configuration (`.busted`)
```lua
return {
_all = {
coverage = false,
lpath = "src/?.lua;src/?/init.lua",
},
default = {
verbose = true,
output = "utfTerminal",
ROOT = { "tests/" },
},
ci = {
ROOT = { "tests/" },
output = "TAP",
coverage = true,
},
}
```
## Unit Test Patterns
### Basic describe / it / before / after
```lua
describe("QuestieDB", function()
local QuestieDB
before_each(function()
-- Fresh module instance per test (isolation)
package.loaded["Database.QuestieDB"] = nil
QuestieDB = require("Database.QuestieDB")
QuestieDB.npcData = {
[1] = { "Hogger", 11, 0 },
[26680] = { "Grizzly Hills NPC", 74, 1 },
}
end)
after_each(function()
package.loaded["Database.QuestieDB"] = nil
end)
describe(":GetNPC", function()
it("returns NPC data for a valid id", function()
local data = QuestieDB:GetNPC(1)
assert.is_not_nil(data)
assert.are.equal("Hogger", data.name)
end)
it("returns nil for an unknown id", function()
assert.is_nil(QuestieDB:GetNPC(99999))
end)
it("returns nil for nil input", function()
assert.is_nil(QuestieDB:GetNPC(nil))
end)
it("returns nil for non-numeric input", function()
assert.is_nil(QuestieDB:GetNPC("abc"))
end)
end)
end)
```
### Pending Tests (Work-in-Progress)
Mark incomplete tests with `pending` — they appear in reports but do not fail:
```lua
pending("respects NPC blacklist during query")
pending("handles WotLK-only NPCs when plugin is absent")
```
### Test Naming Conventions
Use descriptive names that explain the scenario and expected outcome:
```lua
-- GOOD: Explains what + when + expected outcome
it("returns nil when NPC id does not exist in the database", ...)
it("triggers recompile when WotLK plugin is newly installed", ...)
it("skips cleanup when database has not been compiled yet", ...)
-- BAD: Vague
it("works", ...)
it("test 1", ...)
```
## Assertions Reference (Busted)
| Assertion | Meaning |
|-----------|---------|
| `assert.are.equal(expected, actual)` | Strict equality (`==`) |
| `assert.are.same(t1, t2)` | Deep table value equality |
| `assert.is_nil(v)` | `v == nil` |
| `assert.is_not_nil(v)` | `v ~= nil` |
| `assert.is_true(v)` | `v == true` (strict, not truthy) |
| `assert.is_false(v)` | `v == false` (strict, not falsy) |
| `assert.truthy(v)` | `v` is truthy (not `nil`/`false`) |
| `assert.falsy(v)` | `v` is falsy (`nil` or `false`) |
| `assert.has_error(fn)` | `fn()` throws any error |
| `assert.has_error(fn, "msg")` | `fn()` throws with specific message |
| `assert.has_no_error(fn)` | `fn()` does not throw |
| `assert.are.near(expected, actual, tolerance)` | Float comparison within epsilon |
## Mocking and Spies
### spy.on — Observe without replacing
```lua
local s = spy.on(Questie, "Debug")
QuestieDB:GetNPC(0)
assert.spy(s).was.called()
assert.spy(s).was.called_with(
match._, -- self (Questie)
Questie.DEBUG_CRITICAL, -- severity
match._ -- message string
)
s:revert() -- Restore original (automatic in after_each)
```
### stub — Replace with controlled implementation
```lua
stub(QuestieDB, "QueryNPCSingle").returns(nil)
-- Verify it was called with specific args
assert.stub(QuestieDB.QueryNPCSingle).was.called_with(
match._, 26680, match._
)
QuestieDB.QueryNPCSingle:revert()
```
### mock — Full module replacement
```lua
local mockDB = mock({
GetNPC = function(_, id) return { name = "Mock NPC " .. id } end,
GetQuest = function() return nil end,
})
```
**Rule**: Always restore stubs and spies. Busted auto-reverts stubs created with `stub()` at the end of each `it()` block, but manually-created stubs need explicit `:revert()`.
## WoW API Mock Environment
Create a `tests/mocks/wow_api.lua` that defines the WoW protected API surface. Require it BEFORE any addon file:
```lua
-- tests/mocks/wow_api.lua
-- Frame system
_G.CreateFrame = function(frameType, name, parent, template)
local frame = {
_events = {},
_scripts = {},
RegisterEvent = function(self, event) self._events[event] = true end,
UnregisterEvent = function(self, event) self._events[event] = nil end,
SetScript = function(self, handler, fn) self._scripts[handler] = fn end,
GetScript = function(self, handler) return self._scripts[handler] end,
Show = function() end,
Hide = function() end,
IsShown = function() return true end,
}
return frame
end
-- Timer system
_G.C_Timer = {
After = function(delay, fn) fn() end, -- Execute immediately in tests
NewTicker = function(interval, fn, iterations)
for i = 1, (iterations or 1) do fn() end
return { Cancel = function() end }
end,
}
-- Time
_G.GetTime = function() return os.clock() end
_G.time = os.time
-- Unit info
_G.UnitGUID = function(unit) return "Player-1234-ABCDEF" end
_G.UnitName = function(unit) return "TestPlayer" end
_G.UnitLevel = function(unit) return 80 end
_G.UnitFactionGroup = function(unit) return "Alliance", "Alliance" end
-- Map API
_G.C_Map = {
GetMapInfo = function(mapID) return { mapID = mapID, name = "Test Zone" } end,
GetBestMapForUnit = function(unit) return 1 end,
}
-- Enum system
_G.Enum = {
UIMapType = { Cosmic = 0, World = 1, Continent = 2, Zone = 3, Dungeon = 4 },
}
-- Misc
_G.IsAddOnLoaded = function(name) return true end
_G.GetAddOnInfo = function(name) return name, "Test Addon", "", true, "INSECURE" end
_G.InCombatLockdown = function() return false end
_G.debugstack = function(level) return "mock stack trace" end
_G.geterrorhandler = function() return print end
_G.hooksecurefunc = function(table, name, fn) end
_G.print = print
_G.wipe = function(t) for k in pairs(t) do t[k] = nil end return t end
_G.select = select
_G.format = string.format
_G.strsplit = function(sep, str)
local parts = {}
local pattern = "([^" .. sep .. "]+)"
for match in str:gmatch(pattern) do
table.insert(parts, match)
end
return unpack(parts)
end
-- SavedVariables
_G.QuestieSV = {}
```
### Firing Events in Tests
```lua
-- Helper to fire a WoW event on a frame mock
local function fireEvent(frame, event, ...)
if frame._events[event] then
local onEvent = frame._scripts["OnEvent"]
if onEvent then
onEvent(frame, event, ...)
end
end
end
-- Usage in test
it("handles PLAYER_LOGIN event", function()
local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_LOGIN")
frame:SetScript("OnEvent", myHandler)
fireEvent(frame, "PLAYER_LOGIN")
-- assert expected side effects
end)
```
## Error Path Testing
**Always test error paths, not just happy paths:**
```lua
describe("error handling", function()
it("handles nil rawdata gracefully", function()
QuestieDB.npcData = {}
local result = QuestieDB:GetNPC(99999)
assert.is_nil(result)
end)
it("logs critical error for nil rawdata", function()
local s = spy.on(Questie, "Debug")
QuestieDB.npcData = {}
QuestieDB:GetNPC(99999)
assert.spy(s).was.called_with(
match._, Questie.DEBUG_CRITICAL, match.is_string()
)
end)
it("survives pcall on corrupted data", function()
QuestieDB.npcData = { [1] = "not a table" }
assert.has_no_error(function()
QuestieDB:GetNPC(1)
end)
end)
end)
```
## Coverage
Use **Luacov** for line coverage. Target ≥ 80%.
### Configuration (`.luacov`)
```lua
return {
statsfile = "luacov.stats.out",
reportfile = "luacov.report.out",
exclude = {
"tests/", -- Exclude test files from coverage
"Database/Data/.*Data", -- Exclude large static data tables
"Localization/Translations/", -- Exclude translation strings
},
include = {
"Database/",
"Modules/",
},
}
```
### Commands
```bash
# Run tests with coverage
busted --coverage
# Generate report
luacov
cat luacov.report.out
# Fail CI if below threshold
awk '/^Total/ { if ($4+0 < 80) { print "Coverage below 80%: " $4 "%"; exit 1 } }' luacov.report.out
```
## Test-Driven Development Workflow (Lua)
1. **RED** — Write the test first. It must FAIL.
2. **GREEN** — Write the minimum production code to make it pass.
3. **REFACTOR** — Clean up while keeping tests green.
4. **COVERAGE** — Verify ≥ 80% with `busted --coverage && luacov`.
```bash
# TDD cycle
busted tests/unit/QuestieDB_spec.lua # RED: expect failures
# ... write implementation ...
busted tests/unit/QuestieDB_spec.lua # GREEN: expect passes
# ... refactor ...
busted --coverage && luacov # COVERAGE: verify 80%+
```
## Testing Commands
```bash
busted # Run all tests
busted --verbose # Verbose output
busted tests/unit/ # Run only unit tests
busted --filter="GetNPC" # Run tests matching pattern
busted --tags="wotlk" # Run tagged tests only
busted --coverage # With coverage collection
busted --output=TAP # TAP format for CI
busted --shuffle # Randomize test order (detect coupling)
```
## Agent Support
- **tdd-guide** — Use proactively for new features; enforces write-tests-first workflow
- **code-reviewer** — Review test quality after writing tests
+46
View File
@@ -0,0 +1,46 @@
---
paths:
- "**/*.pl"
- "**/*.pm"
- "**/*.t"
- "**/*.psgi"
- "**/*.cgi"
---
# Perl Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Perl-specific content.
## Standards
- Always `use v5.36` (enables `strict`, `warnings`, `say`, subroutine signatures)
- Use subroutine signatures — never unpack `@_` manually
- Prefer `say` over `print` with explicit newlines
## Immutability
- Use **Moo** with `is => 'ro'` and `Types::Standard` for all attributes
- Never use blessed hashrefs directly — always use Moo/Moose accessors
- **OO override note**: Moo `has` attributes with `builder` or `default` are acceptable for computed read-only values
## Formatting
Use **perltidy** with these settings:
```
-i=4 # 4-space indent
-l=100 # 100 char line length
-ce # cuddled else
-bar # opening brace always right
```
## Linting
Use **perlcritic** at severity 3 with themes: `core`, `pbp`, `security`.
```bash
perlcritic --severity 3 --theme 'core || pbp || security' lib/
```
## Reference
See skill: `perl-patterns` for comprehensive modern Perl idioms and best practices.
+22
View File
@@ -0,0 +1,22 @@
---
paths:
- "**/*.pl"
- "**/*.pm"
- "**/*.t"
- "**/*.psgi"
- "**/*.cgi"
---
# Perl Hooks
> This file extends [common/hooks.md](../common/hooks.md) with Perl-specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **perltidy**: Auto-format `.pl` and `.pm` files after edit
- **perlcritic**: Run lint check after editing `.pm` files
## Warnings
- Warn about `print` in non-script `.pm` files — use `say` or a logging module (e.g., `Log::Any`)
+76
View File
@@ -0,0 +1,76 @@
---
paths:
- "**/*.pl"
- "**/*.pm"
- "**/*.t"
- "**/*.psgi"
- "**/*.cgi"
---
# Perl Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Perl-specific content.
## Repository Pattern
Use **DBI** or **DBIx::Class** behind an interface:
```perl
package MyApp::Repo::User;
use Moo;
has dbh => (is => 'ro', required => 1);
sub find_by_id ($self, $id) {
my $sth = $self->dbh->prepare('SELECT * FROM users WHERE id = ?');
$sth->execute($id);
return $sth->fetchrow_hashref;
}
```
## DTOs / Value Objects
Use **Moo** classes with **Types::Standard** (equivalent to Python dataclasses):
```perl
package MyApp::DTO::User;
use Moo;
use Types::Standard qw(Str Int);
has name => (is => 'ro', isa => Str, required => 1);
has email => (is => 'ro', isa => Str, required => 1);
has age => (is => 'ro', isa => Int);
```
## Resource Management
- Always use **three-arg open** with `autodie`
- Use **Path::Tiny** for file operations
```perl
use autodie;
use Path::Tiny;
my $content = path('config.json')->slurp_utf8;
```
## Module Interface
Use `Exporter 'import'` with `@EXPORT_OK` — never `@EXPORT`:
```perl
use Exporter 'import';
our @EXPORT_OK = qw(parse_config validate_input);
```
## Dependency Management
Use **cpanfile** + **carton** for reproducible installs:
```bash
carton install
carton exec prove -lr t/
```
## Reference
See skill: `perl-patterns` for comprehensive modern Perl patterns and idioms.
+69
View File
@@ -0,0 +1,69 @@
---
paths:
- "**/*.pl"
- "**/*.pm"
- "**/*.t"
- "**/*.psgi"
- "**/*.cgi"
---
# Perl Security
> This file extends [common/security.md](../common/security.md) with Perl-specific content.
## Taint Mode
- Use `-T` flag on all CGI/web-facing scripts
- Sanitize `%ENV` (`$ENV{PATH}`, `$ENV{CDPATH}`, etc.) before any external command
## Input Validation
- Use allowlist regex for untainting — never `/(.*)/s`
- Validate all user input with explicit patterns:
```perl
if ($input =~ /\A([a-zA-Z0-9_-]+)\z/) {
my $clean = $1;
}
```
## File I/O
- **Three-arg open only** — never two-arg open
- Prevent path traversal with `Cwd::realpath`:
```perl
use Cwd 'realpath';
my $safe_path = realpath($user_path);
die "Path traversal" unless $safe_path =~ m{\A/allowed/directory/};
```
## Process Execution
- Use **list-form `system()`** — never single-string form
- Use **IPC::Run3** for capturing output
- Never use backticks with variable interpolation
```perl
system('grep', '-r', $pattern, $directory); # safe
```
## SQL Injection Prevention
Always use DBI placeholders — never interpolate into SQL:
```perl
my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?');
$sth->execute($email);
```
## Security Scanning
Run **perlcritic** with the security theme at severity 4+:
```bash
perlcritic --severity 4 --theme security lib/
```
## Reference
See skill: `perl-security` for comprehensive Perl security patterns, taint mode, and safe I/O.
+54
View File
@@ -0,0 +1,54 @@
---
paths:
- "**/*.pl"
- "**/*.pm"
- "**/*.t"
- "**/*.psgi"
- "**/*.cgi"
---
# Perl Testing
> This file extends [common/testing.md](../common/testing.md) with Perl-specific content.
## Framework
Use **Test2::V0** for new projects (not Test::More):
```perl
use Test2::V0;
is($result, 42, 'answer is correct');
done_testing;
```
## Runner
```bash
prove -l t/ # adds lib/ to @INC
prove -lr -j8 t/ # recursive, 8 parallel jobs
```
Always use `-l` to ensure `lib/` is on `@INC`.
## Coverage
Use **Devel::Cover** — target 80%+:
```bash
cover -test
```
## Mocking
- **Test::MockModule** — mock methods on existing modules
- **Test::MockObject** — create test doubles from scratch
## Pitfalls
- Always end test files with `done_testing`
- Never forget the `-l` flag with `prove`
## Reference
See skill: `perl-testing` for detailed Perl TDD patterns with Test2::V0, prove, and Devel::Cover.
+40
View File
@@ -0,0 +1,40 @@
---
paths:
- "**/*.php"
- "**/composer.json"
---
# PHP Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with PHP specific content.
## Standards
- Follow **PSR-12** formatting and naming conventions.
- Prefer `declare(strict_types=1);` in application code.
- Use scalar type hints, return types, and typed properties everywhere new code permits.
## Immutability
- Prefer immutable DTOs and value objects for data crossing service boundaries.
- Use `readonly` properties or immutable constructors for request/response payloads where possible.
- Keep arrays for simple maps; promote business-critical structures into explicit classes.
## Formatting
- Use **PHP-CS-Fixer** or **Laravel Pint** for formatting.
- Use **PHPStan** or **Psalm** for static analysis.
- Keep Composer scripts checked in so the same commands run locally and in CI.
## Imports
- Add `use` statements for all referenced classes, interfaces, and traits.
- Avoid relying on the global namespace unless the project explicitly prefers fully qualified names.
## Error Handling
- Throw exceptions for exceptional states; avoid returning `false`/`null` as hidden error channels in new code.
- Convert framework/request input into validated DTOs before it reaches domain logic.
## Reference
See skill: `backend-patterns` for broader service/repository layering guidance.
+24
View File
@@ -0,0 +1,24 @@
---
paths:
- "**/*.php"
- "**/composer.json"
- "**/phpstan.neon"
- "**/phpstan.neon.dist"
- "**/psalm.xml"
---
# PHP Hooks
> This file extends [common/hooks.md](../common/hooks.md) with PHP specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **Pint / PHP-CS-Fixer**: Auto-format edited `.php` files.
- **PHPStan / Psalm**: Run static analysis after PHP edits in typed codebases.
- **PHPUnit / Pest**: Run targeted tests for touched files or modules when edits affect behavior.
## Warnings
- Warn on `var_dump`, `dd`, `dump`, or `die()` left in edited files.
- Warn when edited PHP files add raw SQL or disable CSRF/session protections.
+33
View File
@@ -0,0 +1,33 @@
---
paths:
- "**/*.php"
- "**/composer.json"
---
# PHP Patterns
> This file extends [common/patterns.md](../common/patterns.md) with PHP specific content.
## Thin Controllers, Explicit Services
- Keep controllers focused on transport: auth, validation, serialization, status codes.
- Move business rules into application/domain services that are easy to test without HTTP bootstrapping.
## DTOs and Value Objects
- Replace shape-heavy associative arrays with DTOs for requests, commands, and external API payloads.
- Use value objects for money, identifiers, date ranges, and other constrained concepts.
## Dependency Injection
- Depend on interfaces or narrow service contracts, not framework globals.
- Pass collaborators through constructors so services are testable without service-locator lookups.
## Boundaries
- Isolate ORM models from domain decisions when the model layer is doing more than persistence.
- Wrap third-party SDKs behind small adapters so the rest of the codebase depends on your contract, not theirs.
## Reference
See skill: `api-design` for endpoint conventions and response-shape guidance.
See skill: `laravel-patterns` for Laravel-specific architecture guidance.
+37
View File
@@ -0,0 +1,37 @@
---
paths:
- "**/*.php"
- "**/composer.lock"
- "**/composer.json"
---
# PHP Security
> This file extends [common/security.md](../common/security.md) with PHP specific content.
## Input and Output
- Validate request input at the framework boundary (`FormRequest`, Symfony Validator, or explicit DTO validation).
- Escape output in templates by default; treat raw HTML rendering as an exception that must be justified.
- Never trust query params, cookies, headers, or uploaded file metadata without validation.
## Database Safety
- Use prepared statements (`PDO`, Doctrine, Eloquent query builder) for all dynamic queries.
- Avoid string-building SQL in controllers/views.
- Scope ORM mass-assignment carefully and whitelist writable fields.
## Secrets and Dependencies
- Load secrets from environment variables or a secret manager, never from committed config files.
- Run `composer audit` in CI and review new package maintainer trust before adding dependencies.
- Pin major versions deliberately and remove abandoned packages quickly.
## Auth and Session Safety
- Use `password_hash()` / `password_verify()` for password storage.
- Regenerate session identifiers after authentication and privilege changes.
- Enforce CSRF protection on state-changing web requests.
## Reference
See skill: `laravel-security` for Laravel-specific security guidance.
+39
View File
@@ -0,0 +1,39 @@
---
paths:
- "**/*.php"
- "**/phpunit.xml"
- "**/phpunit.xml.dist"
- "**/composer.json"
---
# PHP Testing
> This file extends [common/testing.md](../common/testing.md) with PHP specific content.
## Framework
Use **PHPUnit** as the default test framework. If **Pest** is configured in the project, prefer Pest for new tests and avoid mixing frameworks.
## Coverage
```bash
vendor/bin/phpunit --coverage-text
# or
vendor/bin/pest --coverage
```
Prefer **pcov** or **Xdebug** in CI, and keep coverage thresholds in CI rather than as tribal knowledge.
## Test Organization
- Separate fast unit tests from framework/database integration tests.
- Use factory/builders for fixtures instead of large hand-written arrays.
- Keep HTTP/controller tests focused on transport and validation; move business rules into service-level tests.
## Inertia
If the project uses Inertia.js, prefer `assertInertia` with `AssertableInertia` to verify component names and props instead of raw JSON assertions.
## Reference
See skill: `tdd-workflow` for the repo-wide RED -> GREEN -> REFACTOR loop.
See skill: `laravel-tdd` for Laravel-specific testing patterns (PHPUnit and Pest).
+42
View File
@@ -0,0 +1,42 @@
---
paths:
- "**/*.py"
- "**/*.pyi"
---
# Python Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Python specific content.
## Standards
- Follow **PEP 8** conventions
- Use **type annotations** on all function signatures
## Immutability
Prefer immutable data structures:
```python
from dataclasses import dataclass
@dataclass(frozen=True)
class User:
name: str
email: str
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
```
## Formatting
- **black** for code formatting
- **isort** for import sorting
- **ruff** for linting
## Reference
See skill: `python-patterns` for comprehensive Python idioms and patterns.
+19
View File
@@ -0,0 +1,19 @@
---
paths:
- "**/*.py"
- "**/*.pyi"
---
# Python Hooks
> This file extends [common/hooks.md](../common/hooks.md) with Python specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **black/ruff**: Auto-format `.py` files after edit
- **mypy/pyright**: Run type checking after editing `.py` files
## Warnings
- Warn about `print()` statements in edited files (use `logging` module instead)
+39
View File
@@ -0,0 +1,39 @@
---
paths:
- "**/*.py"
- "**/*.pyi"
---
# Python Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Python specific content.
## Protocol (Duck Typing)
```python
from typing import Protocol
class Repository(Protocol):
def find_by_id(self, id: str) -> dict | None: ...
def save(self, entity: dict) -> dict: ...
```
## Dataclasses as DTOs
```python
from dataclasses import dataclass
@dataclass
class CreateUserRequest:
name: str
email: str
age: int | None = None
```
## Context Managers & Generators
- Use context managers (`with` statement) for resource management
- Use generators for lazy evaluation and memory-efficient iteration
## Reference
See skill: `python-patterns` for comprehensive patterns including decorators, concurrency, and package organization.
+30
View File
@@ -0,0 +1,30 @@
---
paths:
- "**/*.py"
- "**/*.pyi"
---
# Python Security
> This file extends [common/security.md](../common/security.md) with Python specific content.
## Secret Management
```python
import os
from dotenv import load_dotenv
load_dotenv()
api_key = os.environ["OPENAI_API_KEY"] # Raises KeyError if missing
```
## Security Scanning
- Use **bandit** for static security analysis:
```bash
bandit -r src/
```
## Reference
See skill: `django-security` for Django-specific security guidelines (if applicable).
+38
View File
@@ -0,0 +1,38 @@
---
paths:
- "**/*.py"
- "**/*.pyi"
---
# Python Testing
> This file extends [common/testing.md](../common/testing.md) with Python specific content.
## Framework
Use **pytest** as the testing framework.
## Coverage
```bash
pytest --cov=src --cov-report=term-missing
```
## Test Organization
Use `pytest.mark` for test categorization:
```python
import pytest
@pytest.mark.unit
def test_calculate_total():
...
@pytest.mark.integration
def test_database_connection():
...
```
## Reference
See skill: `python-testing` for detailed pytest patterns and fixtures.
+151
View File
@@ -0,0 +1,151 @@
---
paths:
- "**/*.rs"
---
# Rust Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Rust-specific content.
## Formatting
- **rustfmt** for enforcement — always run `cargo fmt` before committing
- **clippy** for lints — `cargo clippy -- -D warnings` (treat warnings as errors)
- 4-space indent (rustfmt default)
- Max line width: 100 characters (rustfmt default)
## Immutability
Rust variables are immutable by default — embrace this:
- Use `let` by default; only use `let mut` when mutation is required
- Prefer returning new values over mutating in place
- Use `Cow<'_, T>` when a function may or may not need to allocate
```rust
use std::borrow::Cow;
// GOOD — immutable by default, new value returned
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input)
}
}
// BAD — unnecessary mutation
fn normalize_bad(input: &mut String) {
*input = input.replace(' ', "_");
}
```
## Naming
Follow standard Rust conventions:
- `snake_case` for functions, methods, variables, modules, crates
- `PascalCase` (UpperCamelCase) for types, traits, enums, type parameters
- `SCREAMING_SNAKE_CASE` for constants and statics
- Lifetimes: short lowercase (`'a`, `'de`) — descriptive names for complex cases (`'input`)
## Ownership and Borrowing
- Borrow (`&T`) by default; take ownership only when you need to store or consume
- Never clone to satisfy the borrow checker without understanding the root cause
- Accept `&str` over `String`, `&[T]` over `Vec<T>` in function parameters
- Use `impl Into<String>` for constructors that need to own a `String`
```rust
// GOOD — borrows when ownership isn't needed
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
// GOOD — takes ownership in constructor via Into
fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
// BAD — takes String when &str suffices
fn word_count_bad(text: String) -> usize {
text.split_whitespace().count()
}
```
## Error Handling
- Use `Result<T, E>` and `?` for propagation — never `unwrap()` in production code
- **Libraries**: define typed errors with `thiserror`
- **Applications**: use `anyhow` for flexible error context
- Add context with `.with_context(|| format!("failed to ..."))?`
- Reserve `unwrap()` / `expect()` for tests and truly unreachable states
```rust
// GOOD — library error with thiserror
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("failed to read config: {0}")]
Io(#[from] std::io::Error),
#[error("invalid config format: {0}")]
Parse(String),
}
// GOOD — application error with anyhow
use anyhow::Context;
fn load_config(path: &str) -> anyhow::Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {path}"))?;
toml::from_str(&content)
.with_context(|| format!("failed to parse {path}"))
}
```
## Iterators Over Loops
Prefer iterator chains for transformations; use loops for complex control flow:
```rust
// GOOD — declarative and composable
let active_emails: Vec<&str> = users.iter()
.filter(|u| u.is_active)
.map(|u| u.email.as_str())
.collect();
// GOOD — loop for complex logic with early returns
for user in &users {
if let Some(verified) = verify_email(&user.email)? {
send_welcome(&verified)?;
}
}
```
## Module Organization
Organize by domain, not by type:
```text
src/
├── main.rs
├── lib.rs
├── auth/ # Domain module
│ ├── mod.rs
│ ├── token.rs
│ └── middleware.rs
├── orders/ # Domain module
│ ├── mod.rs
│ ├── model.rs
│ └── service.rs
└── db/ # Infrastructure
├── mod.rs
└── pool.rs
```
## Visibility
- Default to private; use `pub(crate)` for internal sharing
- Only mark `pub` what is part of the crate's public API
- Re-export public API from `lib.rs`
## References
See skill: `rust-patterns` for comprehensive Rust idioms and patterns.
+16
View File
@@ -0,0 +1,16 @@
---
paths:
- "**/*.rs"
- "**/Cargo.toml"
---
# Rust Hooks
> This file extends [common/hooks.md](../common/hooks.md) with Rust-specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **cargo fmt**: Auto-format `.rs` files after edit
- **cargo clippy**: Run lint checks after editing Rust files
- **cargo check**: Verify compilation after changes (faster than `cargo build`)
+168
View File
@@ -0,0 +1,168 @@
---
paths:
- "**/*.rs"
---
# Rust Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Rust-specific content.
## Repository Pattern with Traits
Encapsulate data access behind a trait:
```rust
pub trait OrderRepository: Send + Sync {
fn find_by_id(&self, id: u64) -> Result<Option<Order>, StorageError>;
fn find_all(&self) -> Result<Vec<Order>, StorageError>;
fn save(&self, order: &Order) -> Result<Order, StorageError>;
fn delete(&self, id: u64) -> Result<(), StorageError>;
}
```
Concrete implementations handle storage details (Postgres, SQLite, in-memory for tests).
## Service Layer
Business logic in service structs; inject dependencies via constructor:
```rust
pub struct OrderService {
repo: Box<dyn OrderRepository>,
payment: Box<dyn PaymentGateway>,
}
impl OrderService {
pub fn new(repo: Box<dyn OrderRepository>, payment: Box<dyn PaymentGateway>) -> Self {
Self { repo, payment }
}
pub fn place_order(&self, request: CreateOrderRequest) -> anyhow::Result<OrderSummary> {
let order = Order::from(request);
self.payment.charge(order.total())?;
let saved = self.repo.save(&order)?;
Ok(OrderSummary::from(saved))
}
}
```
## Newtype Pattern for Type Safety
Prevent argument mix-ups with distinct wrapper types:
```rust
struct UserId(u64);
struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) -> anyhow::Result<Order> {
// Can't accidentally swap user and order IDs at call sites
todo!()
}
```
## Enum State Machines
Model states as enums — make illegal states unrepresentable:
```rust
enum ConnectionState {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: String },
Failed { reason: String, retries: u32 },
}
fn handle(state: &ConnectionState) {
match state {
ConnectionState::Disconnected => connect(),
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
ConnectionState::Connecting { .. } => wait(),
ConnectionState::Connected { session_id } => use_session(session_id),
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
ConnectionState::Failed { reason, .. } => log_failure(reason),
}
}
```
Always match exhaustively — no wildcard `_` for business-critical enums.
## Builder Pattern
Use for structs with many optional parameters:
```rust
pub struct ServerConfig {
host: String,
port: u16,
max_connections: usize,
}
impl ServerConfig {
pub fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
ServerConfigBuilder {
host: host.into(),
port,
max_connections: 100,
}
}
}
pub struct ServerConfigBuilder {
host: String,
port: u16,
max_connections: usize,
}
impl ServerConfigBuilder {
pub fn max_connections(mut self, n: usize) -> Self {
self.max_connections = n;
self
}
pub fn build(self) -> ServerConfig {
ServerConfig {
host: self.host,
port: self.port,
max_connections: self.max_connections,
}
}
}
```
## Sealed Traits for Extensibility Control
Use a private module to seal a trait, preventing external implementations:
```rust
mod private {
pub trait Sealed {}
}
pub trait Format: private::Sealed {
fn encode(&self, data: &[u8]) -> Vec<u8>;
}
pub struct Json;
impl private::Sealed for Json {}
impl Format for Json {
fn encode(&self, data: &[u8]) -> Vec<u8> { todo!() }
}
```
## API Response Envelope
Consistent API responses using a generic enum:
```rust
#[derive(Debug, serde::Serialize)]
#[serde(tag = "status")]
pub enum ApiResponse<T: serde::Serialize> {
#[serde(rename = "ok")]
Ok { data: T },
#[serde(rename = "error")]
Error { message: String },
}
```
## References
See skill: `rust-patterns` for comprehensive patterns including ownership, traits, generics, concurrency, and async.
+141
View File
@@ -0,0 +1,141 @@
---
paths:
- "**/*.rs"
---
# Rust Security
> This file extends [common/security.md](../common/security.md) with Rust-specific content.
## Secrets Management
- Never hardcode API keys, tokens, or credentials in source code
- Use environment variables: `std::env::var("API_KEY")`
- Fail fast if required secrets are missing at startup
- Keep `.env` files in `.gitignore`
```rust
// BAD
const API_KEY: &str = "sk-abc123...";
// GOOD — environment variable with early validation
fn load_api_key() -> anyhow::Result<String> {
std::env::var("PAYMENT_API_KEY")
.context("PAYMENT_API_KEY must be set")
}
```
## SQL Injection Prevention
- Always use parameterized queries — never format user input into SQL strings
- Use query builder or ORM (sqlx, diesel, sea-orm) with bind parameters
```rust
// BAD — SQL injection via format string
let query = format!("SELECT * FROM users WHERE name = '{name}'");
sqlx::query(&query).fetch_one(&pool).await?;
// GOOD — parameterized query with sqlx
// Placeholder syntax varies by backend: Postgres: $1 | MySQL: ? | SQLite: $1
sqlx::query("SELECT * FROM users WHERE name = $1")
.bind(&name)
.fetch_one(&pool)
.await?;
```
## Input Validation
- Validate all user input at system boundaries before processing
- Use the type system to enforce invariants (newtype pattern)
- Parse, don't validate — convert unstructured data to typed structs at the boundary
- Reject invalid input with clear error messages
```rust
// Parse, don't validate — invalid states are unrepresentable
pub struct Email(String);
impl Email {
pub fn parse(input: &str) -> Result<Self, ValidationError> {
let trimmed = input.trim();
let at_pos = trimmed.find('@')
.filter(|&p| p > 0 && p < trimmed.len() - 1)
.ok_or_else(|| ValidationError::InvalidEmail(input.to_string()))?;
let domain = &trimmed[at_pos + 1..];
if trimmed.len() > 254 || !domain.contains('.') {
return Err(ValidationError::InvalidEmail(input.to_string()));
}
// For production use, prefer a validated email crate (e.g., `email_address`)
Ok(Self(trimmed.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
```
## Unsafe Code
- Minimize `unsafe` blocks — prefer safe abstractions
- Every `unsafe` block must have a `// SAFETY:` comment explaining the invariant
- Never use `unsafe` to bypass the borrow checker for convenience
- Audit all `unsafe` code during review — it is a red flag without justification
- Prefer `safe` FFI wrappers around C libraries
```rust
// GOOD — safety comment documents ALL required invariants
let widget: &Widget = {
// SAFETY: `ptr` is non-null, aligned, points to an initialized Widget,
// and no mutable references or mutations exist for its lifetime.
unsafe { &*ptr }
};
// BAD — no safety justification
unsafe { &*ptr }
```
## Dependency Security
- Run `cargo audit` to scan for known CVEs in dependencies
- Run `cargo deny check` for license and advisory compliance
- Use `cargo tree` to audit transitive dependencies
- Keep dependencies updated — set up Dependabot or Renovate
- Minimize dependency count — evaluate before adding new crates
```bash
# Security audit
cargo audit
# Deny advisories, duplicate versions, and restricted licenses
cargo deny check
# Inspect dependency tree
cargo tree
cargo tree -d # Show duplicates only
```
## Error Messages
- Never expose internal paths, stack traces, or database errors in API responses
- Log detailed errors server-side; return generic messages to clients
- Use `tracing` or `log` for structured server-side logging
```rust
// Map errors to appropriate status codes and generic messages
// (Example uses axum; adapt the response type to your framework)
match order_service.find_by_id(id) {
Ok(order) => Ok((StatusCode::OK, Json(order))),
Err(ServiceError::NotFound(_)) => {
tracing::info!(order_id = id, "order not found");
Err((StatusCode::NOT_FOUND, "Resource not found"))
}
Err(e) => {
tracing::error!(order_id = id, error = %e, "unexpected error");
Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"))
}
}
```
## References
See skill: `rust-patterns` for unsafe code guidelines and ownership patterns.
See skill: `security-review` for general security checklists.
+154
View File
@@ -0,0 +1,154 @@
---
paths:
- "**/*.rs"
---
# Rust Testing
> This file extends [common/testing.md](../common/testing.md) with Rust-specific content.
## Test Framework
- **`#[test]`** with `#[cfg(test)]` modules for unit tests
- **rstest** for parameterized tests and fixtures
- **proptest** for property-based testing
- **mockall** for trait-based mocking
- **`#[tokio::test]`** for async tests
## Test Organization
```text
my_crate/
├── src/
│ ├── lib.rs # Unit tests in #[cfg(test)] modules
│ ├── auth/
│ │ └── mod.rs # #[cfg(test)] mod tests { ... }
│ └── orders/
│ └── service.rs # #[cfg(test)] mod tests { ... }
├── tests/ # Integration tests (each file = separate binary)
│ ├── api_test.rs
│ ├── db_test.rs
│ └── common/ # Shared test utilities
│ └── mod.rs
└── benches/ # Criterion benchmarks
└── benchmark.rs
```
Unit tests go inside `#[cfg(test)]` modules in the same file. Integration tests go in `tests/`.
## Unit Test Pattern
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creates_user_with_valid_email() {
let user = User::new("Alice", "alice@example.com").unwrap();
assert_eq!(user.name, "Alice");
}
#[test]
fn rejects_invalid_email() {
let result = User::new("Bob", "not-an-email");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid email"));
}
}
```
## Parameterized Tests
```rust
use rstest::rstest;
#[rstest]
#[case("hello", 5)]
#[case("", 0)]
#[case("rust", 4)]
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
assert_eq!(input.len(), expected);
}
```
## Async Tests
```rust
#[tokio::test]
async fn fetches_data_successfully() {
let client = TestClient::new().await;
let result = client.get("/data").await;
assert!(result.is_ok());
}
```
## Mocking with mockall
Define traits in production code; generate mocks in test modules:
```rust
// Production trait — pub so integration tests can import it
pub trait UserRepository {
fn find_by_id(&self, id: u64) -> Option<User>;
}
#[cfg(test)]
mod tests {
use super::*;
use mockall::predicate::eq;
mockall::mock! {
pub Repo {}
impl UserRepository for Repo {
fn find_by_id(&self, id: u64) -> Option<User>;
}
}
#[test]
fn service_returns_user_when_found() {
let mut mock = MockRepo::new();
mock.expect_find_by_id()
.with(eq(42))
.times(1)
.returning(|_| Some(User { id: 42, name: "Alice".into() }));
let service = UserService::new(Box::new(mock));
let user = service.get_user(42).unwrap();
assert_eq!(user.name, "Alice");
}
}
```
## Test Naming
Use descriptive names that explain the scenario:
- `creates_user_with_valid_email()`
- `rejects_order_when_insufficient_stock()`
- `returns_none_when_not_found()`
## Coverage
- Target 80%+ line coverage
- Use **cargo-llvm-cov** for coverage reporting
- Focus on business logic — exclude generated code and FFI bindings
```bash
cargo llvm-cov # Summary
cargo llvm-cov --html # HTML report
cargo llvm-cov --fail-under-lines 80 # Fail if below threshold
```
## Testing Commands
```bash
cargo test # Run all tests
cargo test -- --nocapture # Show println output
cargo test test_name # Run tests matching pattern
cargo test --lib # Unit tests only
cargo test --test api_test # Specific integration test (tests/api_test.rs)
cargo test --doc # Doc tests only
```
## References
See skill: `rust-testing` for comprehensive testing patterns including property-based testing, fixtures, and benchmarking with Criterion.
+47
View File
@@ -0,0 +1,47 @@
---
paths:
- "**/*.swift"
- "**/Package.swift"
---
# Swift Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Swift specific content.
## Formatting
- **SwiftFormat** for auto-formatting, **SwiftLint** for style enforcement
- `swift-format` is bundled with Xcode 16+ as an alternative
## Immutability
- Prefer `let` over `var` — define everything as `let` and only change to `var` if the compiler requires it
- Use `struct` with value semantics by default; use `class` only when identity or reference semantics are needed
## Naming
Follow [Apple API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/):
- Clarity at the point of use — omit needless words
- Name methods and properties for their roles, not their types
- Use `static let` for constants over global constants
## Error Handling
Use typed throws (Swift 6+) and pattern matching:
```swift
func load(id: String) throws(LoadError) -> Item {
guard let data = try? read(from: path) else {
throw .fileNotFound(id)
}
return try decode(data)
}
```
## Concurrency
Enable Swift 6 strict concurrency checking. Prefer:
- `Sendable` value types for data crossing isolation boundaries
- Actors for shared mutable state
- Structured concurrency (`async let`, `TaskGroup`) over unstructured `Task {}`
+20
View File
@@ -0,0 +1,20 @@
---
paths:
- "**/*.swift"
- "**/Package.swift"
---
# Swift Hooks
> This file extends [common/hooks.md](../common/hooks.md) with Swift specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **SwiftFormat**: Auto-format `.swift` files after edit
- **SwiftLint**: Run lint checks after editing `.swift` files
- **swift build**: Type-check modified packages after edit
## Warning
Flag `print()` statements — use `os.Logger` or structured logging instead for production code.
+66
View File
@@ -0,0 +1,66 @@
---
paths:
- "**/*.swift"
- "**/Package.swift"
---
# Swift Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Swift specific content.
## Protocol-Oriented Design
Define small, focused protocols. Use protocol extensions for shared defaults:
```swift
protocol Repository: Sendable {
associatedtype Item: Identifiable & Sendable
func find(by id: Item.ID) async throws -> Item?
func save(_ item: Item) async throws
}
```
## Value Types
- Use structs for data transfer objects and models
- Use enums with associated values to model distinct states:
```swift
enum LoadState<T: Sendable>: Sendable {
case idle
case loading
case loaded(T)
case failed(Error)
}
```
## Actor Pattern
Use actors for shared mutable state instead of locks or dispatch queues:
```swift
actor Cache<Key: Hashable & Sendable, Value: Sendable> {
private var storage: [Key: Value] = [:]
func get(_ key: Key) -> Value? { storage[key] }
func set(_ key: Key, value: Value) { storage[key] = value }
}
```
## Dependency Injection
Inject protocols with default parameters — production uses defaults, tests inject mocks:
```swift
struct UserService {
private let repository: any UserRepository
init(repository: any UserRepository = DefaultUserRepository()) {
self.repository = repository
}
}
```
## References
See skill: `swift-actor-persistence` for actor-based persistence patterns.
See skill: `swift-protocol-di-testing` for protocol-based DI and testing.
+33
View File
@@ -0,0 +1,33 @@
---
paths:
- "**/*.swift"
- "**/Package.swift"
---
# Swift Security
> This file extends [common/security.md](../common/security.md) with Swift specific content.
## Secret Management
- Use **Keychain Services** for sensitive data (tokens, passwords, keys) — never `UserDefaults`
- Use environment variables or `.xcconfig` files for build-time secrets
- Never hardcode secrets in source — decompilation tools extract them trivially
```swift
let apiKey = ProcessInfo.processInfo.environment["API_KEY"]
guard let apiKey, !apiKey.isEmpty else {
fatalError("API_KEY not configured")
}
```
## Transport Security
- App Transport Security (ATS) is enforced by default — do not disable it
- Use certificate pinning for critical endpoints
- Validate all server certificates
## Input Validation
- Sanitize all user input before display to prevent injection
- Use `URL(string:)` with validation rather than force-unwrapping
- Validate data from external sources (APIs, deep links, pasteboard) before processing
+45
View File
@@ -0,0 +1,45 @@
---
paths:
- "**/*.swift"
- "**/Package.swift"
---
# Swift Testing
> This file extends [common/testing.md](../common/testing.md) with Swift specific content.
## Framework
Use **Swift Testing** (`import Testing`) for new tests. Use `@Test` and `#expect`:
```swift
@Test("User creation validates email")
func userCreationValidatesEmail() throws {
#expect(throws: ValidationError.invalidEmail) {
try User(email: "not-an-email")
}
}
```
## Test Isolation
Each test gets a fresh instance — set up in `init`, tear down in `deinit`. No shared mutable state between tests.
## Parameterized Tests
```swift
@Test("Validates formats", arguments: ["json", "xml", "csv"])
func validatesFormat(format: String) throws {
let parser = try Parser(format: format)
#expect(parser.isValid)
}
```
## Coverage
```bash
swift test --enable-code-coverage
```
## Reference
See skill: `swift-protocol-di-testing` for protocol-based dependency injection and mock patterns with Swift Testing.
+199
View File
@@ -0,0 +1,199 @@
---
paths:
- "**/*.ts"
- "**/*.tsx"
- "**/*.js"
- "**/*.jsx"
---
# TypeScript/JavaScript Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with TypeScript/JavaScript specific content.
## Types and Interfaces
Use types to make public APIs, shared models, and component props explicit, readable, and reusable.
### Public APIs
- Add parameter and return types to exported functions, shared utilities, and public class methods
- Let TypeScript infer obvious local variable types
- Extract repeated inline object shapes into named types or interfaces
```typescript
// WRONG: Exported function without explicit types
export function formatUser(user) {
return `${user.firstName} ${user.lastName}`
}
// CORRECT: Explicit types on public APIs
interface User {
firstName: string
lastName: string
}
export function formatUser(user: User): string {
return `${user.firstName} ${user.lastName}`
}
```
### Interfaces vs. Type Aliases
- Use `interface` for object shapes that may be extended or implemented
- Use `type` for unions, intersections, tuples, mapped types, and utility types
- Prefer string literal unions over `enum` unless an `enum` is required for interoperability
```typescript
interface User {
id: string
email: string
}
type UserRole = 'admin' | 'member'
type UserWithRole = User & {
role: UserRole
}
```
### Avoid `any`
- Avoid `any` in application code
- Use `unknown` for external or untrusted input, then narrow it safely
- Use generics when a value's type depends on the caller
```typescript
// WRONG: any removes type safety
function getErrorMessage(error: any) {
return error.message
}
// CORRECT: unknown forces safe narrowing
function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message
}
return 'Unexpected error'
}
```
### React Props
- Define component props with a named `interface` or `type`
- Type callback props explicitly
- Do not use `React.FC` unless there is a specific reason to do so
```typescript
interface User {
id: string
email: string
}
interface UserCardProps {
user: User
onSelect: (id: string) => void
}
function UserCard({ user, onSelect }: UserCardProps) {
return <button onClick={() => onSelect(user.id)}>{user.email}</button>
}
```
### JavaScript Files
- In `.js` and `.jsx` files, use JSDoc when types improve clarity and a TypeScript migration is not practical
- Keep JSDoc aligned with runtime behavior
```javascript
/**
* @param {{ firstName: string, lastName: string }} user
* @returns {string}
*/
export function formatUser(user) {
return `${user.firstName} ${user.lastName}`
}
```
## Immutability
Use spread operator for immutable updates:
```typescript
interface User {
id: string
name: string
}
// WRONG: Mutation
function updateUser(user: User, name: string): User {
user.name = name // MUTATION!
return user
}
// CORRECT: Immutability
function updateUser(user: Readonly<User>, name: string): User {
return {
...user,
name
}
}
```
## Error Handling
Use async/await with try-catch and narrow unknown errors safely:
```typescript
interface User {
id: string
email: string
}
declare function riskyOperation(userId: string): Promise<User>
function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message
}
return 'Unexpected error'
}
const logger = {
error: (message: string, error: unknown) => {
// Replace with your production logger (for example, pino or winston).
}
}
async function loadUser(userId: string): Promise<User> {
try {
const result = await riskyOperation(userId)
return result
} catch (error: unknown) {
logger.error('Operation failed', error)
throw new Error(getErrorMessage(error))
}
}
```
## Input Validation
Use Zod for schema-based validation and infer types from the schema:
```typescript
import { z } from 'zod'
const userSchema = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(150)
})
type UserInput = z.infer<typeof userSchema>
const validated: UserInput = userSchema.parse(input)
```
## Console.log
- No `console.log` statements in production code
- Use proper logging libraries instead
- See hooks for automatic detection
+22
View File
@@ -0,0 +1,22 @@
---
paths:
- "**/*.ts"
- "**/*.tsx"
- "**/*.js"
- "**/*.jsx"
---
# TypeScript/JavaScript Hooks
> This file extends [common/hooks.md](../common/hooks.md) with TypeScript/JavaScript specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **Prettier**: Auto-format JS/TS files after edit
- **TypeScript check**: Run `tsc` after editing `.ts`/`.tsx` files
- **console.log warning**: Warn about `console.log` in edited files
## Stop Hooks
- **console.log audit**: Check all modified files for `console.log` before session ends
+52
View File
@@ -0,0 +1,52 @@
---
paths:
- "**/*.ts"
- "**/*.tsx"
- "**/*.js"
- "**/*.jsx"
---
# TypeScript/JavaScript Patterns
> This file extends [common/patterns.md](../common/patterns.md) with TypeScript/JavaScript specific content.
## API Response Format
```typescript
interface ApiResponse<T> {
success: boolean
data?: T
error?: string
meta?: {
total: number
page: number
limit: number
}
}
```
## Custom Hooks Pattern
```typescript
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
```
## Repository Pattern
```typescript
interface Repository<T> {
findAll(filters?: Filters): Promise<T[]>
findById(id: string): Promise<T | null>
create(data: CreateDto): Promise<T>
update(id: string, data: UpdateDto): Promise<T>
delete(id: string): Promise<void>
}
```
+28
View File
@@ -0,0 +1,28 @@
---
paths:
- "**/*.ts"
- "**/*.tsx"
- "**/*.js"
- "**/*.jsx"
---
# TypeScript/JavaScript Security
> This file extends [common/security.md](../common/security.md) with TypeScript/JavaScript specific content.
## Secret Management
```typescript
// NEVER: Hardcoded secrets
const apiKey = "sk-proj-xxxxx"
// ALWAYS: Environment variables
const apiKey = process.env.OPENAI_API_KEY
if (!apiKey) {
throw new Error('OPENAI_API_KEY not configured')
}
```
## Agent Support
- Use **security-reviewer** skill for comprehensive security audits
+18
View File
@@ -0,0 +1,18 @@
---
paths:
- "**/*.ts"
- "**/*.tsx"
- "**/*.js"
- "**/*.jsx"
---
# TypeScript/JavaScript Testing
> This file extends [common/testing.md](../common/testing.md) with TypeScript/JavaScript specific content.
## E2E Testing
Use **Playwright** as the E2E testing framework for critical user flows.
## Agent Support
- **e2e-runner** - Playwright E2E testing specialist