From fd9453f6eebbfa5552f6a02f4b4cf2b3d0799b2c Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Mon, 11 May 2026 21:34:30 -0400 Subject: [PATCH] docs: salvage F# agent and language guidance --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- AGENTS.md | 7 +- README.md | 14 +- README.zh-CN.md | 2 +- agent.yaml | 1 + agents/fsharp-reviewer.md | 100 ++++++++++ docs/zh-CN/AGENTS.md | 6 +- docs/zh-CN/README.md | 10 +- manifests/install-components.json | 16 ++ manifests/install-modules.json | 1 + package.json | 1 + rules/fsharp/coding-style.md | 112 +++++++++++ rules/fsharp/hooks.md | 26 +++ rules/fsharp/patterns.md | 111 +++++++++++ rules/fsharp/security.md | 76 ++++++++ rules/fsharp/testing.md | 62 ++++++ scripts/lib/install-manifests.js | 2 + scripts/lib/project-detect.js | 5 + skills/fsharp-testing/SKILL.md | 280 ++++++++++++++++++++++++++++ tests/lib/install-manifests.test.js | 12 ++ tests/lib/project-detect.test.js | 15 ++ 22 files changed, 843 insertions(+), 20 deletions(-) create mode 100644 agents/fsharp-reviewer.md create mode 100644 rules/fsharp/coding-style.md create mode 100644 rules/fsharp/hooks.md create mode 100644 rules/fsharp/patterns.md create mode 100644 rules/fsharp/security.md create mode 100644 rules/fsharp/testing.md create mode 100644 skills/fsharp-testing/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ed300ff0..b12310b5 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ { "name": "ecc", "source": "./", - "description": "The most comprehensive Claude Code plugin — 55 agents, 208 skills, 72 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning", + "description": "The most comprehensive Claude Code plugin — 56 agents, 209 skills, 72 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning", "version": "2.0.0-rc.1", "author": { "name": "Affaan Mustafa", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 230f341d..f946782d 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ecc", "version": "2.0.0-rc.1", - "description": "Battle-tested Claude Code plugin for engineering teams — 55 agents, 208 skills, 72 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use", + "description": "Battle-tested Claude Code plugin for engineering teams — 56 agents, 209 skills, 72 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use", "author": { "name": "Affaan Mustafa", "url": "https://x.com/affaanmustafa" diff --git a/AGENTS.md b/AGENTS.md index db3b3a83..a2c7b638 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — Agent Instructions -This is a **production-ready AI coding plugin** providing 55 specialized agents, 208 skills, 72 commands, and automated hook workflows for software development. +This is a **production-ready AI coding plugin** providing 56 specialized agents, 209 skills, 72 commands, and automated hook workflows for software development. **Version:** 2.0.0-rc.1 @@ -27,6 +27,7 @@ This is a **production-ready AI coding plugin** providing 55 specialized agents, | doc-updater | Documentation and codemaps | Updating docs | | cpp-reviewer | C/C++ code review | C and C++ projects | | cpp-build-resolver | C/C++ build errors | C and C++ build failures | +| fsharp-reviewer | F# functional code review | F# projects | | docs-lookup | Documentation lookup via Context7 | API/docs questions | | go-reviewer | Go code review | Go projects | | go-build-resolver | Go build errors | Go build failures | @@ -146,8 +147,8 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat ## Project Structure ``` -agents/ — 55 specialized subagents -skills/ — 208 workflow skills and domain knowledge +agents/ — 56 specialized subagents +skills/ — 209 workflow skills and domain knowledge commands/ — 72 slash commands hooks/ — Trigger-based automations rules/ — Always-follow guidelines (common + per-language) diff --git a/README.md b/README.md index 3da3755d..36a65a82 100644 --- a/README.md +++ b/README.md @@ -358,7 +358,7 @@ If you stacked methods, clean up in this order: /plugin list ecc@ecc ``` -**That's it!** You now have access to 55 agents, 208 skills, and 72 legacy command shims. +**That's it!** You now have access to 56 agents, 209 skills, and 72 legacy command shims. ### Dashboard GUI @@ -456,7 +456,7 @@ everything-claude-code/ | |-- plugin.json # Plugin metadata and component paths | |-- marketplace.json # Marketplace catalog for /plugin marketplace add | -|-- agents/ # 55 specialized subagents for delegation +|-- agents/ # 56 specialized subagents for delegation | |-- planner.md # Feature implementation planning | |-- architect.md # System design decisions | |-- tdd-guide.md # Test-driven development @@ -472,6 +472,7 @@ everything-claude-code/ | |-- harness-optimizer.md # Harness config tuning | |-- cpp-reviewer.md # C++ code review | |-- cpp-build-resolver.md # C++ build error resolution +| |-- fsharp-reviewer.md # F# functional code review | |-- go-reviewer.md # Go code review | |-- go-build-resolver.md # Go build error resolution | |-- python-reviewer.md # Python code review @@ -986,6 +987,7 @@ Not sure where to start? Use this quick reference. Skills are the canonical work | Update documentation | `/update-docs` | doc-updater | | Review Go code | `/go-review` | go-reviewer | | Review Python code | `/python-review` | python-reviewer | +| Review F# code | *(invoke `fsharp-reviewer` directly)* | fsharp-reviewer | | Review TypeScript/JavaScript code | *(invoke `typescript-reviewer` directly)* | typescript-reviewer | | Develop HarmonyOS apps | *(invoke `harmonyos-app-resolver` directly)* | harmonyos-app-resolver | | Audit database queries | *(auto-delegated)* | database-reviewer | @@ -1354,9 +1356,9 @@ The configuration is automatically detected from `.opencode/opencode.json`. | Feature | Claude Code | OpenCode | Status | |---------|-------------|----------|--------| -| Agents | PASS: 55 agents | PASS: 12 agents | **Claude Code leads** | +| Agents | PASS: 56 agents | PASS: 12 agents | **Claude Code leads** | | Commands | PASS: 72 commands | PASS: 35 commands | **Claude Code leads** | -| Skills | PASS: 208 skills | PASS: 37 skills | **Claude Code leads** | +| Skills | PASS: 209 skills | PASS: 37 skills | **Claude Code leads** | | Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** | | Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** | | MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** | @@ -1459,9 +1461,9 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e | Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode | |---------|------------|------------|-----------|----------| -| **Agents** | 55 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | +| **Agents** | 56 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | | **Commands** | 72 | Shared | Instruction-based | 35 | -| **Skills** | 208 | Shared | 10 (native format) | 37 | +| **Skills** | 209 | Shared | 10 (native format) | 37 | | **Hook Events** | 8 types | 15 types | None yet | 11 types | | **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | | **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | diff --git a/README.zh-CN.md b/README.zh-CN.md index ed83c167..14e6f013 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**完成!** 你现在可以使用 55 个代理、208 个技能和 72 个命令。 +**完成!** 你现在可以使用 56 个代理、209 个技能和 72 个命令。 ### multi-* 命令需要额外配置 diff --git a/agent.yaml b/agent.yaml index 0de0e2d0..53c35b17 100644 --- a/agent.yaml +++ b/agent.yaml @@ -68,6 +68,7 @@ skills: - foundation-models-on-device - frontend-patterns - frontend-slides + - fsharp-testing - git-workflow - golang-patterns - golang-testing diff --git a/agents/fsharp-reviewer.md b/agents/fsharp-reviewer.md new file mode 100644 index 00000000..4d852ed0 --- /dev/null +++ b/agents/fsharp-reviewer.md @@ -0,0 +1,100 @@ +--- +name: fsharp-reviewer +description: Expert F# code reviewer specializing in functional idioms, type safety, pattern matching, computation expressions, and performance. Use for all F# code changes. MUST BE USED for F# projects. +tools: ["Read", "Grep", "Glob", "Bash"] +model: sonnet +--- + +You are a senior F# code reviewer ensuring high standards of idiomatic functional F# code and best practices. + +When invoked: +1. Run `git diff -- '*.fs' '*.fsx'` to see recent F# file changes +2. Run `dotnet build` and `fantomas --check .` if available +3. Focus on modified `.fs` and `.fsx` files +4. Begin review immediately + +## Review Priorities + +### CRITICAL - Security +- **SQL Injection**: String concatenation/interpolation in queries - use parameterized queries +- **Command Injection**: Unvalidated input in `Process.Start` - validate and sanitize +- **Path Traversal**: User-controlled file paths - use `Path.GetFullPath` + prefix check +- **Insecure Deserialization**: `BinaryFormatter`, unsafe JSON settings +- **Hardcoded secrets**: API keys, connection strings in source - use configuration/secret manager +- **CSRF/XSS**: Missing anti-forgery tokens, unencoded output in views + +### CRITICAL - Error Handling +- **Swallowed exceptions**: `with _ -> ()` or `with _ -> None` - handle or reraise +- **Missing disposal**: Manual disposal of `IDisposable` - use `use` or `use!` bindings +- **Blocking async**: `.Result`, `.Wait()`, `.GetAwaiter().GetResult()` - use `let!` or `do!` +- **Bare `failwith` in library code**: Prefer `Result` or `Option` for expected failures + +### HIGH - Functional Idioms +- **Mutable state in domain logic**: `mutable`, `ref` cells where immutable alternatives exist +- **Incomplete pattern matches**: Missing cases or catch-all `_` that hides new union cases +- **Imperative loops**: `for`/`while` where `List.map`, `Seq.filter`, `Array.fold` are clearer +- **Null usage**: Using `null` instead of `Option<'T>` for missing values +- **Class-heavy design**: OOP-style classes where modules + functions + records suffice + +### HIGH - Type Safety +- **Primitive obsession**: Raw strings/ints for domain concepts - use single-case DUs +- **Unvalidated input**: Missing validation at system boundaries - use smart constructors +- **Downcasting**: `:?>` without type test - use pattern matching with `:? T as t` +- **`obj` usage**: Avoid `obj` boxing; prefer generics or explicit union types + +### HIGH - Code Quality +- **Large functions**: Over 40 lines - extract helper functions +- **Deep nesting**: More than 3 levels - use early returns, `Result.bind`, or computation expressions +- **Missing `[]`**: On modules/unions that could cause name collisions +- **Unused `open` declarations**: Remove unused module imports + +### MEDIUM - Performance +- **Seq in hot paths**: Lazy sequences recomputed repeatedly - materialize with `Seq.toList` or `Seq.toArray` +- **String concatenation in loops**: Use `StringBuilder` or `String.concat` +- **Excessive boxing**: Value types passed through `obj` - use generic functions +- **N+1 queries**: Lazy loading in loops when using EF Core - use eager loading + +### MEDIUM - Best Practices +- **Naming conventions**: camelCase for functions/values, PascalCase for types/modules/DU cases +- **Pipe operator readability**: Overly long chains - break into named intermediate bindings +- **Computation expression misuse**: Nested `task { task { } }` - flatten with `let!` +- **Module organization**: Related functions scattered across files - group cohesively + +## Diagnostic Commands + +```bash +dotnet build # Compilation check +fantomas --check . # Format check +dotnet test --no-build # Run tests +dotnet test --collect:"XPlat Code Coverage" # Coverage +``` + +## Review Output Format + +```text +[SEVERITY] Issue title +File: path/to/File.fs:42 +Issue: Description +Fix: What to change +``` + +## Approval Criteria + +- **Approve**: No CRITICAL or HIGH issues +- **Warning**: MEDIUM issues only (can merge with caution) +- **Block**: CRITICAL or HIGH issues found + +## Framework Checks + +- **ASP.NET Core**: Giraffe or Saturn handlers, model validation, auth policies, middleware order +- **EF Core**: Migration safety, eager loading, `AsNoTracking` for reads +- **Fable**: Elmish architecture, message handling completeness, view function purity + +## Reference + +For detailed .NET patterns, see skill: `dotnet-patterns`. +For testing guidelines, see skill: `fsharp-testing`. + +--- + +Review with the mindset: "Is this idiomatic F# that leverages the type system and functional patterns effectively?" diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index 63f6ba26..44a10ec5 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — 智能体指令 -这是一个**生产就绪的 AI 编码插件**,提供 55 个专业代理、208 项技能、72 条命令以及自动化钩子工作流,用于软件开发。 +这是一个**生产就绪的 AI 编码插件**,提供 56 个专业代理、209 项技能、72 条命令以及自动化钩子工作流,用于软件开发。 **版本:** 2.0.0-rc.1 @@ -146,8 +146,8 @@ ## 项目结构 ``` -agents/ — 55 个专业子代理 -skills/ — 208 个工作流技能和领域知识 +agents/ — 56 个专业子代理 +skills/ — 209 个工作流技能和领域知识 commands/ — 72 个斜杠命令 hooks/ — 基于触发的自动化 rules/ — 始终遵循的指导方针(通用 + 每种语言) diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index d48df9fa..4df8718f 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**搞定!** 你现在可以使用 55 个智能体、208 项技能和 72 个命令了。 +**搞定!** 你现在可以使用 56 个智能体、209 项技能和 72 个命令了。 *** @@ -1132,9 +1132,9 @@ opencode | 功能特性 | Claude Code | OpenCode | 状态 | |---------|-------------|----------|--------| -| 智能体 | PASS: 55 个 | PASS: 12 个 | **Claude Code 领先** | +| 智能体 | PASS: 56 个 | PASS: 12 个 | **Claude Code 领先** | | 命令 | PASS: 72 个 | PASS: 35 个 | **Claude Code 领先** | -| 技能 | PASS: 208 项 | PASS: 37 项 | **Claude Code 领先** | +| 技能 | PASS: 209 项 | PASS: 37 项 | **Claude Code 领先** | | 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** | | 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** | | MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** | @@ -1240,9 +1240,9 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以 | 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode | |---------|------------|------------|-----------|----------| -| **智能体** | 55 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | +| **智能体** | 56 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | | **命令** | 72 | 共享 | 基于指令 | 35 | -| **技能** | 208 | 共享 | 10 (原生格式) | 37 | +| **技能** | 209 | 共享 | 10 (原生格式) | 37 | | **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 | | **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 | | **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 | diff --git a/manifests/install-components.json b/manifests/install-components.json index d70cc616..28c107f6 100644 --- a/manifests/install-components.json +++ b/manifests/install-components.json @@ -250,6 +250,14 @@ "framework-language" ] }, + { + "id": "lang:fsharp", + "family": "language", + "description": "F# functional patterns and testing guidance. Currently resolves through the shared framework-language module.", + "modules": [ + "framework-language" + ] + }, { "id": "framework:laravel", "family": "framework", @@ -363,6 +371,14 @@ "agents-core" ] }, + { + "id": "agent:fsharp-reviewer", + "family": "agent", + "description": "F# code review agent for functional idioms, type safety, and .NET testing.", + "modules": [ + "agents-core" + ] + }, { "id": "agent:refactor-cleaner", "family": "agent", diff --git a/manifests/install-modules.json b/manifests/install-modules.json index ab230b76..6c37b063 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -128,6 +128,7 @@ "skills/coding-standards", "skills/compose-multiplatform-patterns", "skills/csharp-testing", + "skills/fsharp-testing", "skills/cpp-coding-standards", "skills/cpp-testing", "skills/dart-flutter-patterns", diff --git a/package.json b/package.json index 3ce807c0..7960dd3b 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,7 @@ "skills/foundation-models-on-device/", "skills/frontend-patterns/", "skills/frontend-slides/", + "skills/fsharp-testing/", "skills/github-ops/", "skills/golang-patterns/", "skills/golang-testing/", diff --git a/rules/fsharp/coding-style.md b/rules/fsharp/coding-style.md new file mode 100644 index 00000000..89e29775 --- /dev/null +++ b/rules/fsharp/coding-style.md @@ -0,0 +1,112 @@ +--- +paths: + - "**/*.fs" + - "**/*.fsx" +--- +# F# Coding Style + +> This file extends [common/coding-style.md](../common/coding-style.md) with F#-specific content. + +## Standards + +- Follow standard F# conventions and leverage the type system for correctness +- Prefer immutability by default; use `mutable` only when justified by performance +- Keep modules focused and cohesive + +## Types and Models + +- Prefer discriminated unions for domain modeling over class hierarchies +- Use records for data with named fields +- Use single-case unions for type-safe wrappers around primitives +- Avoid classes unless interop or mutable state requires them + +```fsharp +type EmailAddress = EmailAddress of string + +type OrderStatus = + | Pending + | Confirmed of confirmedAt: DateTimeOffset + | Shipped of trackingNumber: string + | Cancelled of reason: string + +type Order = + { Id: Guid + CustomerId: string + Status: OrderStatus + Items: OrderItem list } +``` + +## Immutability + +- Records are immutable by default; use `with` expressions for updates +- Prefer `list`, `map`, `set` over mutable collections +- Avoid `ref` cells and mutable fields in domain logic + +```fsharp +let rename (profile: UserProfile) newName = + { profile with Name = newName } +``` + +## Function Style + +- Prefer small, composable functions over large methods +- Use the pipe operator `|>` to build readable data pipelines +- Prefer pattern matching over if/else chains +- Use `Option` instead of null; use `Result` for operations that can fail + +```fsharp +let processOrder order = + order + |> validateItems + |> Result.bind calculateTotal + |> Result.map applyDiscount + |> Result.mapError OrderError +``` + +## Async and Error Handling + +- Use `task { }` for interop with .NET async APIs +- Use `async { }` for F#-native async workflows +- Propagate `CancellationToken` through public async APIs +- Prefer `Result` and railway-oriented programming over exceptions for expected failures + +```fsharp +let loadOrderAsync (orderId: Guid) (ct: CancellationToken) = + task { + let! order = repository.FindAsync(orderId, ct) + return + order + |> Option.defaultWith (fun () -> + failwith $"Order {orderId} was not found.") + } +``` + +## Formatting + +- Use `fantomas` for automatic formatting +- Prefer significant whitespace; avoid unnecessary parentheses +- Remove unused `open` declarations + +### Open Declaration Order + +Group `open` statements into four sections separated by a blank line, each section sorted lexically within itself: + +1. `System.*` +2. `Microsoft.*` +3. Third-party namespaces +4. First-party / project namespaces + +```fsharp +open System +open System.Collections.Generic +open System.Threading.Tasks + +open Microsoft.AspNetCore.Http +open Microsoft.Extensions.Logging + +open FsCheck.Xunit +open Swensen.Unquote + +open MyApp.Domain +open MyApp.Infrastructure +``` diff --git a/rules/fsharp/hooks.md b/rules/fsharp/hooks.md new file mode 100644 index 00000000..9108d205 --- /dev/null +++ b/rules/fsharp/hooks.md @@ -0,0 +1,26 @@ +--- +paths: + - "**/*.fs" + - "**/*.fsx" + - "**/*.fsproj" + - "**/*.sln" + - "**/*.slnx" + - "**/Directory.Build.props" + - "**/Directory.Build.targets" +--- +# F# Hooks + +> This file extends [common/hooks.md](../common/hooks.md) with F#-specific content. + +## PostToolUse Hooks + +Configure in `~/.claude/settings.json`: + +- **fantomas**: Auto-format edited F# files +- **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 F# changes +- Warn on modified `appsettings*.json` files so secrets do not get committed diff --git a/rules/fsharp/patterns.md b/rules/fsharp/patterns.md new file mode 100644 index 00000000..490b9950 --- /dev/null +++ b/rules/fsharp/patterns.md @@ -0,0 +1,111 @@ +--- +paths: + - "**/*.fs" + - "**/*.fsx" +--- +# F# Patterns + +> This file extends [common/patterns.md](../common/patterns.md) with F#-specific content. + +## Result Type for Error Handling + +Use `Result<'T, 'TError>` with railway-oriented programming instead of exceptions for expected failures. + +```fsharp +type OrderError = + | InvalidCustomer of string + | EmptyItems + | ItemOutOfStock of sku: string + +let validateOrder (request: CreateOrderRequest) : Result = + if String.IsNullOrWhiteSpace request.CustomerId then + Error(InvalidCustomer "CustomerId is required") + elif request.Items |> List.isEmpty then + Error EmptyItems + else + Ok { CustomerId = request.CustomerId; Items = request.Items } +``` + +## Option for Missing Values + +Prefer `Option<'T>` over null. Use `Option.map`, `Option.bind`, and `Option.defaultValue` to transform. + +```fsharp +let findUser (id: Guid) : User option = + users |> Map.tryFind id + +let getUserEmail userId = + findUser userId + |> Option.map (fun u -> u.Email) + |> Option.defaultValue "unknown@example.com" +``` + +## Discriminated Unions for Domain Modeling + +Model business states explicitly. The compiler enforces exhaustive handling. + +```fsharp +type PaymentState = + | AwaitingPayment of amount: decimal + | Paid of paidAt: DateTimeOffset * transactionId: string + | Refunded of refundedAt: DateTimeOffset * reason: string + | Failed of error: string + +let describePayment = function + | AwaitingPayment amount -> $"Awaiting payment of {amount:C}" + | Paid (at, txn) -> $"Paid at {at} (txn: {txn})" + | Refunded (at, reason) -> $"Refunded at {at}: {reason}" + | Failed error -> $"Payment failed: {error}" +``` + +## Computation Expressions + +Use computation expressions to simplify sequential operations that may fail. + +```fsharp +let placeOrder request = + result { + let! validated = validateOrder request + let! inventory = checkInventory validated.Items + let! order = createOrder validated inventory + return order + } +``` + +## Module Organization + +- Group related functions in modules rather than classes +- Use `[]` to prevent name collisions +- Keep modules small and focused on a single responsibility + +```fsharp +[] +module Order = + let create customerId items = { Id = Guid.NewGuid(); CustomerId = customerId; Items = items; Status = Pending } + let confirm order = { order with Status = Confirmed(DateTimeOffset.UtcNow) } + let cancel reason order = { order with Status = Cancelled reason } +``` + +## Dependency Injection + +- Define dependencies as function parameters or record-of-functions +- Use interfaces sparingly, primarily at the boundary with .NET libraries +- Prefer partial application for injecting dependencies into pipelines + +```fsharp +type OrderDeps = + { FindOrder: Guid -> Task + SaveOrder: Order -> Task + SendNotification: Order -> Task } + +let processOrder (deps: OrderDeps) orderId = + task { + match! deps.FindOrder orderId with + | None -> return Error "Order not found" + | Some order -> + let confirmed = Order.confirm order + do! deps.SaveOrder confirmed + do! deps.SendNotification confirmed + return Ok confirmed + } +``` diff --git a/rules/fsharp/security.md b/rules/fsharp/security.md new file mode 100644 index 00000000..86801c0a --- /dev/null +++ b/rules/fsharp/security.md @@ -0,0 +1,76 @@ +--- +paths: + - "**/*.fs" + - "**/*.fsx" + - "**/*.fsproj" + - "**/appsettings*.json" +--- +# F# Security + +> This file extends [common/security.md](../common/security.md) with F#-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 + +```fsharp +// BAD +let apiKey = "sk-live-123" + +// GOOD +let apiKey = + configuration["OpenAI:ApiKey"] + |> Option.ofObj + |> Option.defaultWith (fun () -> failwith "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 + +```fsharp +let findByCustomer (connection: IDbConnection) customerId = + task { + let sql = "SELECT * FROM Orders WHERE CustomerId = @customerId" + return! connection.QueryAsync(sql, {| customerId = customerId |}) + } +``` + +## Input Validation + +- Validate inputs at the application boundary using types +- Use single-case discriminated unions for validated values +- Reject invalid input before it enters domain logic + +```fsharp +type ValidatedEmail = private ValidatedEmail of string + +module ValidatedEmail = + let create (input: string) = + if System.Text.RegularExpressions.Regex.IsMatch(input, @"^[^@]+@[^@]+\.[^@]+$") then + Ok(ValidatedEmail input) + else + Error "Invalid email address" + + let value (ValidatedEmail v) = v +``` + +## 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. diff --git a/rules/fsharp/testing.md b/rules/fsharp/testing.md new file mode 100644 index 00000000..8dbc7f91 --- /dev/null +++ b/rules/fsharp/testing.md @@ -0,0 +1,62 @@ +--- +paths: + - "**/*.fs" + - "**/*.fsx" + - "**/*.fsproj" +--- +# F# Testing + +> This file extends [common/testing.md](../common/testing.md) with F#-specific content. + +## Test Framework + +- Prefer **xUnit** with **FsUnit.xUnit** for F#-friendly assertions +- Use **Unquote** for quotation-based assertions with clear failure messages +- Use **FsCheck.xUnit** for property-based testing +- Use **NSubstitute** or function stubs 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 + +```fsharp +open Xunit +open Swensen.Unquote + +[] +let ``PlaceOrder returns success when request is valid`` () = + let request = { CustomerId = "cust-123"; Items = [ validItem ] } + let result = OrderService.placeOrder request + test <@ Result.isOk result @> + +[] +let ``PlaceOrder returns error when items are empty`` () = + let request = { CustomerId = "cust-123"; Items = [] } + let result = OrderService.placeOrder request + test <@ Result.isError result @> +``` + +## Property-Based Testing with FsCheck + +```fsharp +open FsCheck.Xunit + +[] +let ``order total is never negative`` (items: OrderItem list) = + let total = Order.calculateTotal items + total >= 0m +``` + +## ASP.NET Core Integration Tests + +- Use `WebApplicationFactory` 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 diff --git a/scripts/lib/install-manifests.js b/scripts/lib/install-manifests.js index 2b677ce7..2fba6677 100644 --- a/scripts/lib/install-manifests.js +++ b/scripts/lib/install-manifests.js @@ -40,6 +40,7 @@ const LEGACY_LANGUAGE_ALIAS_TO_CANONICAL = Object.freeze({ c: 'c', cpp: 'cpp', csharp: 'csharp', + fsharp: 'fsharp', go: 'go', golang: 'go', arkts: 'arkts', @@ -58,6 +59,7 @@ const LEGACY_LANGUAGE_EXTRA_MODULE_IDS = Object.freeze({ c: ['framework-language'], cpp: ['framework-language'], csharp: ['framework-language'], + fsharp: ['framework-language'], go: ['framework-language'], arkts: ['framework-language'], java: ['framework-language'], diff --git a/scripts/lib/project-detect.js b/scripts/lib/project-detect.js index dafd9b34..7c7d605b 100644 --- a/scripts/lib/project-detect.js +++ b/scripts/lib/project-detect.js @@ -60,6 +60,11 @@ const LANGUAGE_RULES = [ markers: [], extensions: ['.cs', '.csproj', '.sln'] }, + { + type: 'fsharp', + markers: [], + extensions: ['.fs', '.fsx', '.fsproj'] + }, { type: 'swift', markers: ['Package.swift'], diff --git a/skills/fsharp-testing/SKILL.md b/skills/fsharp-testing/SKILL.md new file mode 100644 index 00000000..10a187bc --- /dev/null +++ b/skills/fsharp-testing/SKILL.md @@ -0,0 +1,280 @@ +--- +name: fsharp-testing +description: F# testing patterns with xUnit, FsUnit, Unquote, FsCheck property-based testing, integration tests, and test organization best practices. +origin: ECC +--- + +# F# Testing Patterns + +Comprehensive testing patterns for F# applications using xUnit, FsUnit, Unquote, FsCheck, and modern .NET testing practices. + +## When to Activate + +- Writing new tests for F# code +- Reviewing test quality and coverage +- Setting up test infrastructure for F# projects +- Debugging flaky or slow tests + +## Test Framework Stack + +| Tool | Purpose | +|---|---| +| **xUnit** | Test framework (standard .NET ecosystem choice) | +| **FsUnit.xUnit** | F#-friendly assertion syntax for xUnit | +| **Unquote** | Assertion library using F# quotations for clear failure messages | +| **FsCheck.xUnit** | Property-based testing integrated with xUnit | +| **NSubstitute** | Mocking .NET dependencies | +| **Testcontainers** | Real infrastructure in integration tests | +| **WebApplicationFactory** | ASP.NET Core integration tests | + +## Unit Tests with xUnit + FsUnit + +### Basic Test Structure + +```fsharp +module OrderServiceTests + +open Xunit +open FsUnit.Xunit + +[] +let ``create sets status to Pending`` () = + let order = Order.create "cust-1" [ validItem ] + order.Status |> should equal Pending + +[] +let ``confirm changes status to Confirmed`` () = + let order = Order.create "cust-1" [ validItem ] + let confirmed = Order.confirm order + confirmed.Status |> should be (ofCase <@ Confirmed @>) +``` + +### Assertions with Unquote + +Unquote uses F# quotations so failure messages show the full expression that failed, not just "expected X got Y". + +```fsharp +module OrderValidationTests + +open Xunit +open Swensen.Unquote + +[] +let ``PlaceOrder returns success when request is valid`` () = + let request = { CustomerId = "cust-123"; Items = [ validItem ] } + let result = OrderService.placeOrder request + test <@ Result.isOk result @> + +[] +let ``order total sums item prices`` () = + let items = [ { Sku = "A"; Quantity = 2; Price = 10m } + { Sku = "B"; Quantity = 1; Price = 5m } ] + let total = Order.calculateTotal items + test <@ total = 25m @> + +[] +let ``validated email rejects empty input`` () = + let result = ValidatedEmail.create "" + test <@ Result.isError result @> +``` + +### Async Tests + +```fsharp +[] +let ``PlaceOrder returns success when request is valid`` () = task { + let deps = createTestDeps () + let request = { CustomerId = "cust-123"; Items = [ validItem ] } + + let! result = OrderService.placeOrder deps request + + test <@ Result.isOk result @> +} + +[] +let ``PlaceOrder returns error when items are empty`` () = task { + let deps = createTestDeps () + let request = { CustomerId = "cust-123"; Items = [] } + + let! result = OrderService.placeOrder deps request + + test <@ Result.isError result @> +} +``` + +### Parameterized Tests with Theory + +```fsharp +[] +[] +[] +let ``PlaceOrder rejects empty customer ID`` (customerId: string) = + let request = { CustomerId = customerId; Items = [ validItem ] } + let result = OrderService.placeOrder request + result |> should be (ofCase <@ Error @>) + +[] +[] +[] +[] +[] +let ``IsValidEmail returns expected result`` (email: string, expected: bool) = + test <@ EmailValidator.isValid email = expected @> +``` + +## Property-Based Testing with FsCheck + +### Using FsCheck.xUnit + +```fsharp +open FsCheck +open FsCheck.Xunit + +[] +let ``order total is always non-negative`` (items: NonEmptyList) = + let orderItems = + items.Get + |> List.map (fun (qty, price) -> + { Sku = "SKU"; Quantity = qty.Get; Price = abs price }) + let total = Order.calculateTotal orderItems + total >= 0m + +[] +let ``serialization roundtrips`` (order: Order) = + let json = JsonSerializer.Serialize order + let deserialized = JsonSerializer.Deserialize json + deserialized = order +``` + +### Custom Generators + +```fsharp +type OrderGenerators = + static member ValidEmail () = + gen { + let! user = Gen.elements [ "alice"; "bob"; "carol" ] + let! domain = Gen.elements [ "example.com"; "test.org" ] + return $"{user}@{domain}" + } + |> Arb.fromGen + +[ |])>] +let ``valid emails pass validation`` (email: string) = + EmailValidator.isValid email +``` + +## Mocking Dependencies + +### Function Stubs (Preferred) + +```fsharp +let createTestDeps () = + let mutable savedOrders = [] + { FindOrder = fun id -> task { return Map.tryFind id testData } + SaveOrder = fun order -> task { savedOrders <- order :: savedOrders } + SendNotification = fun _ -> Task.CompletedTask } + +[] +let ``PlaceOrder saves the confirmed order`` () = task { + let mutable saved = [] + let deps = + { createTestDeps () with + SaveOrder = fun order -> task { saved <- order :: saved } } + + let! _ = OrderService.placeOrder deps validRequest + + test <@ saved.Length = 1 @> +} +``` + +### NSubstitute for .NET Interfaces + +```fsharp +open NSubstitute + +[] +let ``calls repository with correct ID`` () = task { + let repo = Substitute.For() + repo.FindByIdAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(Some testOrder)) + + let service = OrderService(repo) + let! _ = service.GetOrder(testOrder.Id, CancellationToken.None) + + do! repo.Received(1).FindByIdAsync(testOrder.Id, Arg.Any()) +} +``` + +## ASP.NET Core Integration Tests + +```fsharp +type OrderApiTests (factory: WebApplicationFactory) = + interface IClassFixture> + + let client = + factory.WithWebHostBuilder(fun builder -> + builder.ConfigureServices(fun services -> + services.RemoveAll>() |> ignore + services.AddDbContext(fun options -> + options.UseInMemoryDatabase("TestDb") |> ignore) |> ignore)) + .CreateClient() + + [] + member _.``GET order returns 404 when not found`` () = task { + let! response = client.GetAsync($"/api/orders/{Guid.NewGuid()}") + test <@ response.StatusCode = HttpStatusCode.NotFound @> + } +``` + +## Test Organization + +``` +tests/ + MyApp.Tests/ + Unit/ + OrderServiceTests.fs + PaymentServiceTests.fs + Integration/ + OrderApiTests.fs + OrderRepositoryTests.fs + Properties/ + OrderPropertyTests.fs + Helpers/ + TestData.fs + TestDeps.fs +``` + +## Common Anti-Patterns + +| Anti-Pattern | Fix | +|---|---| +| Testing implementation details | Test behavior and outcomes | +| Mutable shared test state | Fresh state per test | +| `Thread.Sleep` in async tests | Use `Task.Delay` with timeout, or polling helpers | +| Asserting on `sprintf` output | Assert on typed values and pattern matches | +| Ignoring `CancellationToken` | Always pass and verify cancellation | +| Skipping property-based tests | Use FsCheck for any function with clear invariants | + +## Related Skills + +- `dotnet-patterns` - Idiomatic .NET patterns, dependency injection, and architecture +- `csharp-testing` - C# testing patterns (shared infrastructure like WebApplicationFactory and Testcontainers applies to F# too) + +## Running Tests + +```bash +# Run all tests +dotnet test + +# Run with coverage +dotnet test --collect:"XPlat Code Coverage" + +# Run specific project +dotnet test tests/MyApp.Tests/ + +# Filter by test name +dotnet test --filter "FullyQualifiedName~OrderService" + +# Watch mode during development +dotnet watch test --project tests/MyApp.Tests/ +``` diff --git a/tests/lib/install-manifests.test.js b/tests/lib/install-manifests.test.js index 35fd35e0..8ba3006e 100644 --- a/tests/lib/install-manifests.test.js +++ b/tests/lib/install-manifests.test.js @@ -179,6 +179,7 @@ function runTests() { assert.ok(languages.includes('cpp')); assert.ok(languages.includes('c')); assert.ok(languages.includes('csharp')); + assert.ok(languages.includes('fsharp')); })) passed++; else failed++; if (test('resolves a real project profile with target-specific skips', () => { @@ -420,6 +421,17 @@ function runTests() { 'csharp should resolve to framework-language module'); })) passed++; else failed++; + if (test('resolves fsharp legacy compatibility into framework-language module', () => { + const selection = resolveLegacyCompatibilitySelection({ + target: 'cursor', + legacyLanguages: ['fsharp'], + }); + + assert.ok(selection.moduleIds.includes('rules-core')); + assert.ok(selection.moduleIds.includes('framework-language'), + 'fsharp should resolve to framework-language module'); + })) passed++; else failed++; + if (test('keeps antigravity legacy compatibility selections target-safe', () => { const selection = resolveLegacyCompatibilitySelection({ target: 'antigravity', diff --git a/tests/lib/project-detect.test.js b/tests/lib/project-detect.test.js index 0d7aec8d..94830af9 100644 --- a/tests/lib/project-detect.test.js +++ b/tests/lib/project-detect.test.js @@ -234,6 +234,21 @@ function runTests() { } })) passed++; else failed++; + console.log('\nF# Detection:'); + + if (test('detects fsharp from project and source files', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'App.fsproj', ''); + writeTestFile(dir, 'Program.fs', 'printfn "hello"\n'); + const result = detectProjectType(dir); + assert.ok(result.languages.includes('fsharp')); + assert.strictEqual(result.primary, 'fsharp'); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + // Go detection console.log('\nGo Detection:');