diff --git a/opencode/agent/core/openagent.md b/opencode/agent/core/openagent.md index a725614..d193d12 100644 --- a/opencode/agent/core/openagent.md +++ b/opencode/agent/core/openagent.md @@ -4,6 +4,7 @@ description: "Universal agent for answering queries, executing tasks, and coordi mode: primary temperature: 0.2 permission: + question: "allow" bash: "*": "ask" "rm -rf *": "ask" diff --git a/opencode/agent/core/opencoder.md b/opencode/agent/core/opencoder.md index 600c23f..2b33f46 100644 --- a/opencode/agent/core/opencoder.md +++ b/opencode/agent/core/opencoder.md @@ -4,6 +4,7 @@ description: "Orchestration agent for complex coding, architecture, and multi-fi mode: primary temperature: 0.1 permission: + question: "allow" bash: "rm -rf *": "ask" "sudo *": "deny" diff --git a/opencode/command/commit.md b/opencode/command/commit.md index 75b5367..55a5256 100644 --- a/opencode/command/commit.md +++ b/opencode/command/commit.md @@ -4,28 +4,36 @@ description: Create well-formatted commits with conventional commit messages # Commit Command -You are an AI agent that helps create well-formatted git commits with conventional commit messages, follow these instructions exactly. Always run and NEVER push the commit, you don't need to ask for confirmation unless there is a big issue or error. +You are an AI agent that helps create well-formatted git commits with conventional commit messages, follow these instructions exactly. ## Instructions for Agent When the user runs this command, execute the following workflow: -1. **Analyze git status**: +1. **Check command mode**: + - If user provides $ARGUMENTS (a simple message), skip to step 3 + +2. **Run pre-commit validation**: + - Run appropriate tests + - If tests fails, ask user if they want to proceed anyway or fix issues first + +3. **Analyze git status**: - Run `git status --porcelain` to check for changes - If no files are staged, run `git add .` to stage all modified files - If files are already staged, proceed with only those files -2. **Analyze the changes**: +4. **Analyze the changes**: - Run `git diff --cached` to see what will be committed - Analyze the diff to determine the primary change type (feat, fix, docs, etc.) - Identify the main scope and purpose of the changes -3. **Generate commit message**: +5. **Generate commit message**: + - Choose appropriate emoji and type from the reference below - Create message following format: `: ` - Keep description concise, clear, and in imperative mood - Show the proposed message to user for confirmation -4. **Execute the commit**: +6. **Execute the commit**: - Run `git commit -m ""` - Display the commit hash and confirm success - Provide brief summary of what was committed diff --git a/opencode/context/core/standards/csharp-project-structure.md b/opencode/context/core/standards/csharp-project-structure.md new file mode 100644 index 0000000..d9512a3 --- /dev/null +++ b/opencode/context/core/standards/csharp-project-structure.md @@ -0,0 +1,669 @@ +--- +id: csharp-project-structure +name: C# Project Structure +description: "ASP.NET Core project structure with Minimal APIs, CQRS, EF Core, and PostgreSQL patterns" +category: core +type: standard +version: 1.4.0 +author: community +--- + + + +# C# Project Structure Standards + +**Purpose**: Standard project structure for ASP.NET Core APIs using Minimal APIs, CQRS (MediatR), Vertical Slice Architecture, and EF Core with PostgreSQL +**Scope**: Project layout, wiring patterns, and conventions — not language-level rules (see `csharp.md`) +**Last Updated**: 2026-03-15 + +--- + +## Design Principles + +- **`Features/`** contains only business logic — Commands, Queries, Validators, Handlers. No HTTP or framework code. +- **`Features/Common/`** contains shared feature-level concerns — pipeline behaviors, shared exceptions. +- **`Infrastructure/`** contains all technical wiring — endpoint registration, DB context, external services. +- **Command/Query records are the API contract** — they are sent directly from endpoints. No separate DTO mapping layer. + +--- + +## Table of Contents + +1. [Project Initialization](#1-project-initialization) +2. [Project Layout](#2-project-layout) +3. [Program.cs](#3-programcs) +4. [Features — Vertical Slices](#4-features--vertical-slices) +5. [Infrastructure](#5-infrastructure) +6. [EF Core & PostgreSQL](#6-ef-core--postgresql) +7. [Migrations](#7-migrations) +8. [NuGet Packages](#8-nuget-packages) + +--- + +## 1. Project Initialization + +**Always run these commands FIRST when creating a new ASP.NET Core project:** + +```bash +# Create .gitignore (C# patterns) +dotnet new gitignore + +# Create .gitattributes (normalize line endings) +dotnet new gitattributes + +# Create the project +dotnet new web -n MyApi +cd MyApi +``` + +**Why this order?** +- `.gitignore` must exist before any build artifacts are created (prevents committing `bin/`, `obj/`, etc.) +- `.gitattributes` ensures consistent line endings across team members +- Projects created after these are in place benefit from proper version control setup + +--- + +## 2. Project Layout + +``` +MyApi/ +├── Program.cs # DI wiring + endpoint mapping only +├── MyApi.csproj +│ +├── Api/ # Endpoint entry points — discover processes here +│ ├── OrderEndpoints.cs # IEndpointRouteBuilder extension — all /orders routes +│ ├── UserEndpoints.cs +│ └── ProductEndpoints.cs +│ +├── Features/ # Pure business logic — no HTTP/framework code +│ ├── Orders/ +│ │ ├── CreateOrder.cs # Command + Validator + Handler (co-located) +│ │ ├── GetOrder.cs # Query + Handler +│ │ ├── GetAllOrders.cs # Query + Handler +│ │ └── CancelOrder.cs # Command + Handler + Domain Event +│ │ +│ ├── Users/ +│ │ ├── CreateUser.cs +│ │ └── GetUser.cs +│ │ +│ ├── Products/ +│ │ └── CreateProduct.cs +│ │ +│ └── Common/ # Shared feature-level concerns +│ ├── Behaviors/ +│ │ ├── LoggingBehavior.cs +│ │ └── ValidationBehavior.cs +│ └── Exceptions/ +│ ├── NotFoundException.cs +│ └── ValidationException.cs +│ +├── Infrastructure/ # All framework/technical wiring (non-endpoint) +│ ├── Persistence/ +│ │ ├── AppDbContext.cs +│ │ ├── Configurations/ # IEntityTypeConfiguration classes +│ │ │ ├── OrderConfiguration.cs +│ │ │ └── UserConfiguration.cs +│ │ └── Migrations/ # EF Core migrations (auto-generated) +│ ├── Services/ # External HTTP clients, email, storage, etc. +│ └── Extensions/ +│ └── InfrastructureExtensions.cs # AddInfrastructure() registration +│ +├── Domain/ # Optional: rich domain model (for DDD projects) +│ ├── Entities/ +│ └── Events/ +│ +├── appsettings.json +├── appsettings.Development.json +└── GlobalUsings.cs # global using directives +``` + +--- + +## 3. Program.cs + +`Program.cs` contains **only** DI registration and endpoint mapping. No business logic. + +```csharp +// Program.cs +using MyApi.Api; +using MyApi.Infrastructure.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +// ── Infrastructure (DB, external services) ─────────────────────────────── +builder.Services.AddInfrastructure(builder.Configuration); + +// ── MediatR (CQRS) ─────────────────────────────────────────────────────── +builder.Services.AddMediatR(cfg => +{ + cfg.RegisterServicesFromAssembly(typeof(Program).Assembly); + cfg.AddOpenBehavior(typeof(LoggingBehavior<,>)); + cfg.AddOpenBehavior(typeof(ValidationBehavior<,>)); +}); + +// ── Validation ──────────────────────────────────────────────────────────── +builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly); + +// ── OpenAPI ─────────────────────────────────────────────────────────────── +builder.Services.AddOpenApi(); // .NET 9 native; or Swashbuckle for earlier versions + +// ── Auth ────────────────────────────────────────────────────────────────── +builder.Services.AddAuthentication().AddJwtBearer(); +builder.Services.AddAuthorization(); + +// ── Build ───────────────────────────────────────────────────────────────── +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) + app.MapOpenApi(); + +app.UseAuthentication(); +app.UseAuthorization(); + +// ── Endpoint registration ───────────────────────────────────────────────── +app.MapOrderEndpoints(); +app.MapUserEndpoints(); +app.MapProductEndpoints(); + +app.Run(); + +public partial class Program { } // allows WebApplicationFactory in integration tests +``` + +--- + +## 4. Features — Vertical Slices + +Each use case lives in its own file: **Command or Query record + Validator + Handler — nothing else**. +The Command/Query record is the API contract — it is bound directly from the HTTP request body/route. +No separate DTO types, no mapping layer. + +### 3.1 Command Slice (write operation) + +```csharp +// Features/Orders/CreateOrder.cs +namespace MyApi.Features.Orders; + +// ── Command = API request contract ─────────────────────────────────────── +// Bound directly from HTTP request body. No separate DTO needed. +public record CreateOrderCommand(Guid UserId, List Items) : IRequest; + +// Return type is also a record — represents the response shape +public record CreatedOrderResult(Guid Id, Guid UserId, DateTime CreatedAt); + +// ── Validator ───────────────────────────────────────────────────────────── +public class CreateOrderValidator : AbstractValidator +{ + public CreateOrderValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.Items).NotEmpty().WithMessage("Order must contain at least one item."); + } +} + +// ── Handler ─────────────────────────────────────────────────────────────── +public class CreateOrderHandler(AppDbContext db) : IRequestHandler +{ + public async Task Handle(CreateOrderCommand cmd, CancellationToken ct) + { + var order = new Order + { + Id = Guid.NewGuid(), + UserId = cmd.UserId, + Items = cmd.Items, + CreatedAt = DateTime.UtcNow, + }; + + db.Orders.Add(order); + await db.SaveChangesAsync(ct); + + return new CreatedOrderResult(order.Id, order.UserId, order.CreatedAt); + } +} +``` + +### 3.2 Query Slice (read operation) + +```csharp +// Features/Orders/GetOrder.cs +namespace MyApi.Features.Orders; + +// ── Query = API request contract ────────────────────────────────────────── +// Route parameter bound directly. Return type is the response shape. +public record GetOrderQuery(Guid Id) : IRequest; + +public record OrderResult(Guid Id, Guid UserId, OrderStatus Status, DateTime CreatedAt); + +// ── Handler ─────────────────────────────────────────────────────────────── +public class GetOrderHandler(AppDbContext db) : IRequestHandler +{ + public async Task Handle(GetOrderQuery query, CancellationToken ct) + => await db.Orders + .AsNoTracking() + .Where(o => o.Id == query.Id) + .Select(o => new OrderResult(o.Id, o.UserId, o.Status, o.CreatedAt)) + .FirstOrDefaultAsync(ct); +} +``` + +### 3.3 Domain Events (INotification) + +```csharp +// Features/Orders/CancelOrder.cs +namespace MyApi.Features.Orders; + +public record CancelOrderCommand(Guid Id) : IRequest; + +// ── Domain event ────────────────────────────────────────────────────────── +public record OrderCancelledEvent(Guid OrderId) : INotification; + +// ── Handler ─────────────────────────────────────────────────────────────── +public class CancelOrderHandler(AppDbContext db, IPublisher publisher) + : IRequestHandler +{ + public async Task Handle(CancelOrderCommand cmd, CancellationToken ct) + { + var order = await db.Orders.FindAsync([cmd.Id], ct); + if (order is null) return false; + + order.Status = OrderStatus.Cancelled; + await db.SaveChangesAsync(ct); + + await publisher.Publish(new OrderCancelledEvent(order.Id), ct); + return true; + } +} + +// ── Side-effect handlers (each independent, all run on Publish) ────────── +public class SendCancellationEmailHandler(IEmailService email) + : INotificationHandler +{ + public async Task Handle(OrderCancelledEvent e, CancellationToken ct) + => await email.SendCancellationAsync(e.OrderId, ct); +} +``` + +### 3.4 Features/Common — Pipeline Behaviors + +```csharp +// Features/Common/Behaviors/LoggingBehavior.cs +namespace MyApi.Features.Common.Behaviors; + +public class LoggingBehavior(ILogger> logger) + : IPipelineBehavior + where TRequest : notnull +{ + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken ct) + { + var name = typeof(TRequest).Name; + logger.LogInformation("Handling {Request}", name); + var sw = Stopwatch.StartNew(); + try + { + var response = await next(); + logger.LogInformation("Handled {Request} in {ElapsedMs}ms", name, sw.ElapsedMilliseconds); + return response; + } + catch (Exception ex) + { + logger.LogError(ex, "Error handling {Request} after {ElapsedMs}ms", name, sw.ElapsedMilliseconds); + throw; + } + } +} +``` + +```csharp +// Features/Common/Behaviors/ValidationBehavior.cs +namespace MyApi.Features.Common.Behaviors; + +public class ValidationBehavior(IEnumerable> validators) + : IPipelineBehavior + where TRequest : notnull +{ + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken ct) + { + if (!validators.Any()) return await next(); + + var context = new ValidationContext(request); + var results = await Task.WhenAll(validators.Select(v => v.ValidateAsync(context, ct))); + var failures = results.SelectMany(r => r.Errors).Where(f => f is not null).ToList(); + + if (failures.Count > 0) + throw new ValidationException(failures); + + return await next(); + } +} +``` + +--- + +## 5. Infrastructure & API Endpoints + +### 4.1 API Endpoints (Entry Points) + +Endpoint files live in the `Api/` directory at the project root — they are the entry points for discovering business processes. These files handle HTTP concerns only: routing, parameter binding, response shaping, and dispatching to MediatR. No business logic here. + +```csharp +// Api/OrderEndpoints.cs +namespace MyApi.Api; + +public static class OrderEndpoints +{ + public static void MapOrderEndpoints(this IEndpointRouteBuilder app) + { + var group = app.MapGroup("/orders") + .WithTags("Orders") + .WithOpenApi() + .RequireAuthorization(); + + group.MapGet("/", GetAllOrders).WithName("GetAllOrders"); + group.MapGet("/{id}", GetOrder) .WithName("GetOrder"); + group.MapPost("/", CreateOrder) .WithName("CreateOrder"); + group.MapDelete("/{id}", CancelOrder) .WithName("CancelOrder"); + } + + // Command/Query records are bound directly from HTTP — no mapping needed + // Use ISender (not IMediator) — exposes only Send/CreateStream + + private static async Task>> GetAllOrders( + ISender sender, CancellationToken ct) + => TypedResults.Ok(await sender.Send(new GetAllOrdersQuery(), ct)); + + private static async Task, NotFound>> GetOrder( + Guid id, ISender sender, CancellationToken ct) + { + var result = await sender.Send(new GetOrderQuery(id), ct); + return result is not null ? TypedResults.Ok(result) : TypedResults.NotFound(); + } + + private static async Task, ValidationProblem>> CreateOrder( + CreateOrderCommand command, ISender sender, CancellationToken ct) + { + try + { + var order = await sender.Send(command, ct); + return TypedResults.Created($"/orders/{order.Id}", order); + } + catch (ValidationException ex) + { + return TypedResults.ValidationProblem(ex.ToDictionary()); + } + } + + private static async Task> CancelOrder( + Guid id, ISender sender, CancellationToken ct) + { + var cancelled = await sender.Send(new CancelOrderCommand(id), ct); + return cancelled ? TypedResults.NoContent() : TypedResults.NotFound(); + } +} +``` + +### 4.2 InfrastructureExtensions + +```csharp +// Infrastructure/Extensions/InfrastructureExtensions.cs +namespace MyApi.Infrastructure.Extensions; + +public static class InfrastructureExtensions +{ + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, + IConfiguration configuration) + { + // ── Database ────────────────────────────────────────────────────── + services.AddDbContext(options => + options.UseNpgsql( + configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException( + "Connection string 'DefaultConnection' not found."))); + + // ── External services ───────────────────────────────────────────── + services.AddHttpClient(client => + { + client.BaseAddress = new Uri( + configuration["ExternalService:BaseUrl"] + ?? throw new InvalidOperationException("ExternalService:BaseUrl not configured.")); + }); + + services.AddScoped(); + + return services; + } +} +``` + +### 4.3 AppDbContext + +```csharp +// Infrastructure/Persistence/AppDbContext.cs +namespace MyApi.Infrastructure.Persistence; + +public class AppDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Orders => Set(); + public DbSet Users => Set(); + public DbSet Products => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Auto-discovers all IEntityTypeConfiguration classes in the assembly + modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); + } +} +``` + +### 4.4 Entity Configuration + +```csharp +// Infrastructure/Persistence/Configurations/OrderConfiguration.cs +namespace MyApi.Infrastructure.Persistence.Configurations; + +public sealed class OrderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("orders"); + + builder.HasKey(o => o.Id); + + builder.Property(o => o.Id) + .HasDefaultValueSql("gen_random_uuid()"); + + builder.Property(o => o.Status) + .HasConversion() + .HasMaxLength(50) + .IsRequired(); + + builder.Property(o => o.CreatedAt) + .HasDefaultValueSql("now()") + .IsRequired(); + + builder.HasQueryFilter(o => !o.IsDeleted); + + builder.HasOne(o => o.User) + .WithMany(u => u.Orders) + .HasForeignKey(o => o.UserId) + .OnDelete(DeleteBehavior.Restrict); + } +} +``` + +--- + +## 6. EF Core & PostgreSQL + +### 5.1 Connection String (appsettings.json) + +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=myapp;Username=postgres;Password=yourpassword" + } +} +``` + +Override in environment / Docker / Kubernetes using double-underscore notation: +``` +ConnectionStrings__DefaultConnection=Host=prod-db;... +``` + +For local development, use `dotnet user-secrets`: +```bash +dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=localhost;..." +``` + +### 5.2 Async Query Patterns + +```csharp +// ✅ Always pass CancellationToken to all EF async methods +var orders = await db.Orders + .AsNoTracking() // read-only queries: skip change tracking + .Where(o => o.UserId == userId) + .OrderByDescending(o => o.CreatedAt) + .ToListAsync(ct); + +// ✅ FindAsync for primary key lookup (uses change tracker cache first) +var order = await db.Orders.FindAsync([orderId], ct); + +// ✅ Bulk update / delete without loading entities (EF 7+) +await db.Orders + .Where(o => o.Status == OrderStatus.Pending && o.CreatedAt < cutoff) + .ExecuteUpdateAsync(s => s.SetProperty(o => o.Status, OrderStatus.Expired), ct); + +await db.Orders + .Where(o => o.IsDeleted && o.CreatedAt < cutoff) + .ExecuteDeleteAsync(ct); +``` + +### 5.3 AsNoTracking + +```csharp +// ✅ Use AsNoTracking for all read/query handlers — no change tracker overhead +var result = await db.Orders + .AsNoTracking() + .Where(o => o.Id == id) + .Select(o => new OrderResult(o.Id, o.UserId, o.Status, o.CreatedAt)) + .FirstOrDefaultAsync(ct); + +// ✅ Omit AsNoTracking in command handlers that modify and call SaveChangesAsync +var order = await db.Orders.FindAsync([id], ct); +order!.Status = OrderStatus.Shipped; +await db.SaveChangesAsync(ct); +``` + +--- + +## 7. Migrations + +### Common Commands + +```bash +# Add a new migration after model changes +dotnet ef migrations add MigrationName + +# Apply pending migrations to the database +dotnet ef database update + +# List all migrations and their applied status +dotnet ef migrations list + +# Remove the last unapplied migration +dotnet ef migrations remove + +# Multi-project setup (DbContext in separate library) +dotnet ef migrations add MigrationName \ + --project src/MyApp.Data \ + --startup-project src/MyApp.Api +``` + +### Production Deployment + +```bash +# Generate idempotent SQL script — safe to run multiple times (recommended for CI/CD) +dotnet ef migrations script --idempotent --output migrations.sql + +# EF 9: self-contained migration bundle (no dotnet SDK needed at deploy time) +dotnet ef migrations bundle --output migrations-bundle +./migrations-bundle --connection "${DB_CONNECTION_STRING}" +``` + +> **Do not** auto-migrate in `Program.cs` (`db.Database.MigrateAsync()`) in production multi-instance deployments — use SQL scripts or migration bundles instead to avoid race conditions. + +--- + +## 8. NuGet Packages + +```xml + + + + + + + + + + + + + + + + + + + + + + + +``` + +--- + +## GlobalUsings.cs + +```csharp +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; +global using FluentValidation; +global using MediatR; +global using Microsoft.AspNetCore.Http.HttpResults; +global using Microsoft.AspNetCore.Routing; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using MyApi.Infrastructure.Persistence; +global using NSubstitute; +``` + +--- + +## Quick Reference + +| Concern | Where it lives | Rule | +|---------|---------------|------| +| DI wiring + endpoint mapping | `Program.cs` | Declarative only, no logic | +| API entry points (discover processes) | `Api/{Name}Endpoints.cs` | `IEndpointRouteBuilder` extension, routing + dispatch only | +| Business use case | `Features/{Name}/{UseCase}.cs` | Command/Query + Validator + Handler only | +| API request/response contract | Command/Query record + result record | No separate DTOs, no mapping | +| Pipeline behaviors | `Features/Common/Behaviors/` | `IPipelineBehavior` | +| Shared exceptions | `Features/Common/Exceptions/` | Domain exception hierarchy | +| Database context + entity configs | `Infrastructure/Persistence/` | `AppDbContext` + `IEntityTypeConfiguration` | +| EF Core migrations | `Infrastructure/Persistence/Migrations/` | Auto-generated by `dotnet ef` | +| External services | `Infrastructure/Services/` | Interface + implementation | +| Infra DI registration | `Infrastructure/Extensions/InfrastructureExtensions.cs` | `AddInfrastructure()` | +| Global usings | `GlobalUsings.cs` | `global using` | diff --git a/opencode/context/core/standards/csharp.md b/opencode/context/core/standards/csharp.md new file mode 100644 index 0000000..ada8e1d --- /dev/null +++ b/opencode/context/core/standards/csharp.md @@ -0,0 +1,903 @@ +--- +id: csharp +name: C# Standards +description: "Universal C# best practices for naming, async, LINQ, error handling, and testing" +category: core +type: standard +version: 1.1.0 +author: community +--- + + + +# Universal C# Standards + +**Purpose**: Universal C# best practices for AI agents working on .NET projects +**Scope**: Language-level patterns, not framework-specific +**Last Updated**: 2026-03-15 + +--- + +## Table of Contents + +1. [Naming Conventions](#1-naming-conventions) +2. [Type Safety & Nullability](#2-type-safety--nullability) +3. [Async/Await Patterns](#3-asyncawait-patterns) +4. [LINQ](#4-linq) +5. [Error Handling](#5-error-handling) +6. [Pattern Matching](#6-pattern-matching) +7. [Code Organization](#7-code-organization) +8. [Records & Immutability](#8-records--immutability) +9. [Dependency Injection](#9-dependency-injection) +10. [Testing](#10-testing) + +--- + +## 1. Naming Conventions + +### 1.1 General Rules + +| Element | Convention | Example | +|---------|-----------|---------| +| Classes, structs, records | PascalCase | `UserService`, `OrderItem` | +| Interfaces | `I` + PascalCase | `IUserRepository`, `IOrderService` | +| Methods | PascalCase | `GetUserById`, `ProcessOrder` | +| Properties | PascalCase | `FirstName`, `CreatedAt` | +| Fields (private) | `_` + camelCase | `_logger`, `_repository` | +| Local variables | camelCase | `userId`, `orderTotal` | +| Parameters | camelCase | `userId`, `cancellationToken` | +| Constants | PascalCase | `MaxRetryCount`, `DefaultTimeout` | +| Enums | PascalCase (type and members) | `OrderStatus.Pending` | +| Async methods | `Async` suffix | `GetUserAsync`, `SaveOrderAsync` | + +### 1.2 Method Naming + +```csharp +// ✅ GOOD - Verb + noun, PascalCase +public async Task GetUserByIdAsync(Guid userId) { } +public async Task> ListOrdersAsync(Guid userId) { } +public async Task DeleteUserAsync(Guid userId) { } +public bool IsEligibleForDiscount(Order order) { } +public bool HasPermission(string action) { } + +// ❌ AVOID - Ambiguous or wrong case +public async Task fetchuser(Guid id) { } +public async Task DoUserGet(Guid id) { } +``` + +### 1.3 Interface Naming + +```csharp +// ✅ GOOD - Always prefix with I +public interface IUserRepository { } +public interface IOrderService { } +public interface INotificationSender { } + +// ❌ AVOID - No prefix, or wrong prefix +public interface UserRepository { } +public interface TUserRepository { } +``` + +### 1.4 Private Fields + +```csharp +// ✅ GOOD - Underscore prefix, camelCase +public class OrderService +{ + private readonly IOrderRepository _orderRepository; + private readonly ILogger _logger; + private int _retryCount; +} + +// ❌ AVOID - No prefix, or m_ prefix +private IOrderRepository orderRepository; +private IOrderRepository m_orderRepository; +``` + +--- + +## 2. Type Safety & Nullability + +### 2.1 Enable Nullable Reference Types + +**Rule: Always enable nullable reference types in all projects** + +```xml + + + enable + nullable + +``` + +```csharp +// ✅ GOOD - Explicit nullability +public string Name { get; set; } // Non-nullable: must be assigned +public string? MiddleName { get; set; } // Nullable: may be null + +public User? FindUser(Guid id) { } // Returns null if not found +public User GetUser(Guid id) { } // Never returns null (throws if not found) +``` + +### 2.2 Null Handling + +```csharp +// ✅ GOOD - Null-coalescing and conditional operators +var name = user?.Name ?? "Unknown"; +var city = user?.Address?.City ?? string.Empty; +user?.Notify("Welcome"); + +// ✅ GOOD - Null guard at method entry +public void ProcessOrder(Order order) +{ + ArgumentNullException.ThrowIfNull(order); + // ... +} + +// ✅ GOOD - Null-coalescing assignment +_cache ??= new Dictionary(); + +// ❌ AVOID - Manual null checks where operators suffice +if (user != null && user.Address != null) + city = user.Address.City; +``` + +### 2.3 Avoid Primitive Obsession + +```csharp +// ✅ GOOD - Strongly typed IDs prevent mixing up parameters +public readonly record struct UserId(Guid Value); +public readonly record struct OrderId(Guid Value); + +public Task GetOrderAsync(OrderId orderId, UserId userId) { } + +// ❌ AVOID - Raw Guids are easy to mix up +public Task GetOrderAsync(Guid orderId, Guid userId) { } +``` + +--- + +## 3. Async/Await Patterns + +### 3.1 Always Use CancellationToken + +**Rule: Every public async method must accept a CancellationToken** + +```csharp +// ✅ GOOD - CancellationToken flows through all async calls +public async Task GetUserAsync(Guid userId, CancellationToken cancellationToken = default) +{ + var user = await _repository.FindAsync(userId, cancellationToken); + return user ?? throw new NotFoundException($"User {userId} not found"); +} + +// ❌ AVOID - No way to cancel long-running operations +public async Task GetUserAsync(Guid userId) +{ + return await _repository.FindAsync(userId); +} +``` + +### 10.6 When to Use Moq Instead of NSubstitute + +While **NSubstitute is the default**, use **Moq** for: + +```csharp +// Use Moq when you need MockBehavior.Strict (fail on unexpected calls) +var mockRepository = new Mock(MockBehavior.Strict); +mockRepository.Setup(r => r.GetOrderAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new Order()); + +// Use Moq for verifying complex call sequences +var sequence = new MockSequence(); +_serviceA.InSequence(sequence).Setup(x => x.MethodA()).ReturnsAsync(true); +_serviceB.InSequence(sequence).Setup(x => x.MethodB()).ReturnsAsync(false); + +// Use Moq for verifying no other calls were made +mock.VerifyNoOtherCalls(); + +// Use Moq for mocking protected members +mock.Protected().Setup("GetName").Returns("test"); +``` + +**Note**: NSubstitute covers ~95% of real-world test scenarios. Only reach for Moq's advanced features if NSubstitute doesn't provide what you need. + +--- + +## 4. LINQ + +### 4.1 Prefer Method Syntax for Simple Chains + +```csharp +// ✅ GOOD - Method syntax for filter/project/sort +var activeUserNames = users + .Where(u => u.IsActive) + .OrderBy(u => u.LastName) + .Select(u => u.FullName) + .ToList(); + +// ✅ GOOD - Query syntax for complex joins (more readable) +var result = + from order in orders + join user in users on order.UserId equals user.Id + where order.Status == OrderStatus.Pending + select new { order.Id, user.Name }; +``` + +### 4.2 Materialize at the Right Time + +```csharp +// ✅ GOOD - Materialize once, use the list +var activeUsers = users.Where(u => u.IsActive).ToList(); +var count = activeUsers.Count; +var first = activeUsers.FirstOrDefault(); + +// ❌ AVOID - Multiple enumerations of IEnumerable (re-evaluates query each time) +var activeUsers = users.Where(u => u.IsActive); +var count = activeUsers.Count(); // evaluates query +var first = activeUsers.First(); // evaluates query again +``` + +### 4.3 Use Appropriate Termination Methods + +```csharp +// ✅ GOOD - Choose the right method for the intent +var first = items.FirstOrDefault(); // null if empty +var single = items.SingleOrDefault(); // null if empty, throws if >1 +var any = items.Any(x => x.IsActive); // bool, stops at first match +var all = items.All(x => x.IsActive); // bool, fails fast on false +var count = items.Count(x => x.IsActive); // full enumeration + +// ❌ AVOID - Using Count() to check existence (full enumeration) +if (items.Count() > 0) { } // Use Any() instead +``` + +### 4.4 Avoid LINQ in Performance-Critical Paths + +```csharp +// ✅ GOOD - Direct loop when allocation matters +var total = 0m; +foreach (var item in orderItems) + total += item.Price * item.Quantity; + +// LINQ alternative (fine for most code, avoids premature optimization) +var total = orderItems.Sum(item => item.Price * item.Quantity); +``` + +--- + +## 5. Error Handling + +### 5.1 Use Specific Exception Types + +```csharp +// ✅ GOOD - Specific, meaningful exceptions +public async Task GetUserAsync(Guid userId, CancellationToken ct = default) +{ + var user = await _repository.FindAsync(userId, ct); + if (user is null) + throw new NotFoundException($"User '{userId}' was not found."); + return user; +} + +// ✅ GOOD - Domain exception hierarchy +public class DomainException : Exception +{ + public DomainException(string message) : base(message) { } + public DomainException(string message, Exception inner) : base(message, inner) { } +} + +public class NotFoundException : DomainException +{ + public NotFoundException(string message) : base(message) { } +} + +public class ValidationException : DomainException +{ + public IReadOnlyList Errors { get; } + + public ValidationException(IReadOnlyList errors) + : base("One or more validation errors occurred.") + => Errors = errors; +} + +// ❌ AVOID - Generic exceptions with no context +throw new Exception("Not found"); +throw new ApplicationException("Something went wrong"); +``` + +### 5.2 Validate at Entry Points + +```csharp +// ✅ GOOD - Fail fast with guard clauses +public void PlaceOrder(Order order, Guid userId) +{ + ArgumentNullException.ThrowIfNull(order); + ArgumentNullException.ThrowIfNull(userId); + + if (order.Items.Count == 0) + throw new ValidationException(["Order must contain at least one item."]); + + if (order.TotalAmount <= 0) + throw new ValidationException(["Order total must be greater than zero."]); + + // proceed with valid input +} +``` + +### 5.3 Result Pattern (for Expected Failures) + +```csharp +// ✅ GOOD - Use Result when failure is a normal outcome (not exceptional) +public readonly record struct Result +{ + public bool IsSuccess { get; } + public T? Value { get; } + public string? Error { get; } + + private Result(bool isSuccess, T? value, string? error) + { + IsSuccess = isSuccess; + Value = value; + Error = error; + } + + public static Result Success(T value) => new(true, value, null); + public static Result Failure(string error) => new(false, default, error); +} + +// Usage +public Result SubmitOrder(Cart cart) +{ + if (!cart.HasItems) + return Result.Failure("Cart is empty."); + + var order = CreateOrderFromCart(cart); + return Result.Success(order); +} + +var result = SubmitOrder(cart); +if (result.IsSuccess) + Console.WriteLine($"Order created: {result.Value!.Id}"); +else + Console.WriteLine($"Failed: {result.Error}"); + +// Reserve exceptions for truly exceptional, unexpected conditions. +// Use Result for expected business failures (validation, not found in context of search, etc.) +``` + +### 5.4 Catch Specific Exceptions + +```csharp +// ✅ GOOD - Catch what you can handle +try +{ + await _paymentGateway.ChargeAsync(amount, ct); +} +catch (PaymentDeclinedException ex) +{ + _logger.LogWarning(ex, "Payment declined for amount {Amount}", amount); + return Result.Failure("Payment was declined."); +} +catch (TimeoutException ex) +{ + _logger.LogError(ex, "Payment gateway timed out"); + throw; // re-throw: let caller or middleware handle retries +} + +// ❌ AVOID - Swallowing exceptions silently +try { await _paymentGateway.ChargeAsync(amount, ct); } +catch { } + +// ❌ AVOID - Catching Exception broadly without re-throw +catch (Exception ex) +{ + _logger.LogError(ex, "Error"); + return null; // hides the real problem +} +``` + +--- + +## 6. Pattern Matching + +### 6.1 Switch Expressions (Prefer over switch statements) + +```csharp +// ✅ GOOD - Switch expression: concise, exhaustive +public decimal GetDiscount(CustomerTier tier) => tier switch +{ + CustomerTier.Bronze => 0.00m, + CustomerTier.Silver => 0.05m, + CustomerTier.Gold => 0.10m, + CustomerTier.Platinum => 0.20m, + _ => throw new ArgumentOutOfRangeException(nameof(tier), tier, null) +}; + +// ❌ AVOID - Verbose switch statement for simple value mapping +switch (tier) +{ + case CustomerTier.Bronze: return 0.00m; + case CustomerTier.Silver: return 0.05m; + // ... +} +``` + +### 6.2 Type Patterns + +```csharp +// ✅ GOOD - Type pattern with declaration +public string Describe(Shape shape) => shape switch +{ + Circle c => $"Circle with radius {c.Radius}", + Rectangle r => $"Rectangle {r.Width}x{r.Height}", + Triangle t => $"Triangle with base {t.Base}", + _ => "Unknown shape" +}; + +// ✅ GOOD - is pattern for type checking with binding +if (notification is EmailNotification email) +{ + await SendEmailAsync(email.Address, email.Body, ct); +} +``` + +### 6.3 Property Patterns + +```csharp +// ✅ GOOD - Property pattern for readable conditionals +public decimal CalculateShipping(Order order) => order switch +{ + { TotalAmount: >= 100 } => 0m, // free shipping + { IsExpressDelivery: true } => 15m, // express + { ShippingAddress.Country: "FI" } => 5m, // domestic + _ => 10m // international +}; +``` + +### 6.4 Deconstruction + +```csharp +// ✅ GOOD - Deconstruct tuples and records +var (firstName, lastName) = GetFullName(userId); +var (lat, lon) = location; + +// ✅ GOOD - Discard unused parts +var (id, _, createdAt) = GetOrderSummary(orderId); +``` + +--- + +## 7. Code Organization + +### 7.1 Namespace Per Feature (not per type) + +```csharp +// ✅ GOOD - Feature-based namespaces +namespace MyApp.Orders; // all order-related types together +namespace MyApp.Users; // all user-related types together +namespace MyApp.Notifications; + +// ❌ AVOID - Layer-based namespaces that scatter a feature across the codebase +namespace MyApp.Repositories; +namespace MyApp.Services; +namespace MyApp.Controllers; +``` + +### 7.2 File-Scoped Namespaces + +```csharp +// ✅ GOOD - File-scoped namespace (C# 10+): less indentation +namespace MyApp.Orders; + +public class OrderService { } + +// ❌ AVOID - Block-scoped namespace adds unnecessary indentation +namespace MyApp.Orders +{ + public class OrderService { } +} +``` + +### 7.3 One Type Per File + +```csharp +// ✅ GOOD - OrderService.cs contains only OrderService +// ✅ ACCEPTABLE - Small, closely related types in one file (e.g., value objects + their exceptions) + +// ❌ AVOID - Multiple unrelated types in one file +// OrderService.cs containing OrderService + UserService + ProductRepository +``` + +### 7.4 Using Directives + +```csharp +// ✅ GOOD - Global usings for commonly used namespaces (in a GlobalUsings.cs file) +global using System; +global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; +global using Microsoft.Extensions.Logging; + +// ✅ GOOD - File-level usings at the top, outside namespace +using System.Text.Json; +using MyApp.Common; + +namespace MyApp.Orders; +``` + +### 7.5 Class Structure Order + +Follow this order within a class: + +```csharp +public class OrderService : IOrderService +{ + // 1. Constants + private const int MaxRetryCount = 3; + + // 2. Static fields + private static readonly JsonSerializerOptions JsonOptions = new(); + + // 3. Instance fields (private) + private readonly IOrderRepository _repository; + private readonly ILogger _logger; + + // 4. Constructor(s) + public OrderService(IOrderRepository repository, ILogger logger) + { + _repository = repository; + _logger = logger; + } + + // 5. Properties + + // 6. Public methods + + // 7. Private methods +} +``` + +--- + +## 8. Records & Immutability + +### 8.1 Use Records for Value Objects and DTOs + +```csharp +// ✅ GOOD - Record for immutable data (value semantics, built-in equality) +public record UserDto(Guid Id, string Name, string Email); + +public record Address(string Street, string City, string PostalCode, string Country); + +// ✅ GOOD - Readonly record struct for small value objects (stack allocated) +public readonly record struct Money(decimal Amount, string Currency); +public readonly record struct Coordinates(double Latitude, double Longitude); + +// Non-destructive mutation via 'with' +var updated = originalUser with { Email = "new@example.com" }; +``` + +### 8.2 Immutable Collections + +```csharp +// ✅ GOOD - Expose immutable views +public class Order +{ + private readonly List _items = new(); + + public IReadOnlyList Items => _items.AsReadOnly(); + + public void AddItem(OrderItem item) + { + ArgumentNullException.ThrowIfNull(item); + _items.Add(item); + } +} + +// ✅ GOOD - ImmutableList for truly immutable scenarios +using System.Collections.Immutable; + +public record ShoppingCart(ImmutableList Items) +{ + public ShoppingCart AddItem(CartItem item) => + this with { Items = Items.Add(item) }; + + public ShoppingCart RemoveItem(Guid itemId) => + this with { Items = Items.RemoveAll(i => i.Id == itemId) }; +} +``` + +### 8.3 init-only Properties + +```csharp +// ✅ GOOD - init allows construction but prevents later mutation +public class OrderConfiguration +{ + public Guid OrderId { get; init; } + public string Currency { get; init; } = "EUR"; + public int MaxItems { get; init; } = 100; +} + +// Can set during object initializer, but not after +var config = new OrderConfiguration { OrderId = Guid.NewGuid(), Currency = "USD" }; +// config.Currency = "EUR"; // ❌ compile error +``` + +--- + +## 9. Dependency Injection + +### 9.1 Constructor Injection (Preferred) + +```csharp +// ✅ GOOD - All dependencies injected through constructor, stored readonly +public class OrderService : IOrderService +{ + private readonly IOrderRepository _repository; + private readonly IPaymentGateway _paymentGateway; + private readonly ILogger _logger; + + public OrderService( + IOrderRepository repository, + IPaymentGateway paymentGateway, + ILogger logger) + { + _repository = repository; + _paymentGateway = paymentGateway; + _logger = logger; + } +} + +// ❌ AVOID - Service locator pattern (hidden dependencies) +public class OrderService +{ + public async Task ProcessAsync() + { + var repo = ServiceLocator.Get(); // hidden dependency + } +} +``` + +### 9.2 Lifetime Registration + +```csharp +// Registration in Program.cs or an extension method +services.AddScoped(); // per HTTP request +services.AddTransient(); // new instance each time +services.AddSingleton(); // single instance + +// ✅ GOOD - Extension method groups related registrations +public static class OrdersServiceCollectionExtensions +{ + public static IServiceCollection AddOrders(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + return services; + } +} + +// In Program.cs +builder.Services.AddOrders(); +``` + +### 9.3 Options Pattern for Configuration + +```csharp +// ✅ GOOD - Strongly-typed configuration +public class PaymentOptions +{ + public const string SectionName = "Payment"; + + public string ApiKey { get; set; } = string.Empty; + public string BaseUrl { get; set; } = string.Empty; + public int TimeoutSeconds { get; set; } = 30; +} + +// Registration +builder.Services.Configure( + builder.Configuration.GetSection(PaymentOptions.SectionName)); + +// Usage +public class PaymentGateway +{ + private readonly PaymentOptions _options; + + public PaymentGateway(IOptions options) + { + _options = options.Value; + } +} +``` + +--- + +## 10. Testing + +### 10.1 Framework & Structure + +**Use xUnit** as the default test framework. Use **Shouldly** for readable assertions. Use **NSubstitute** for mocking (pragmatic, readable syntax). Use **Moq** only for scenarios requiring strict mock behavior or complex verification. + +```csharp +// ✅ GOOD - xUnit test class structure with NSubstitute +public class OrderServiceTests +{ + // Arrange shared fixtures in constructor or use class fixtures + private readonly IOrderRepository _repositorySubstitute = Substitute.For(); + private readonly ILogger _loggerSubstitute = Substitute.For>(); + private readonly OrderService _sut; + + public OrderServiceTests() + { + _sut = new OrderService(_repositorySubstitute, _loggerSubstitute); + } + + [Fact] + public async Task GetOrderAsync_WhenOrderExists_ReturnsOrder() + { + // Arrange + var orderId = Guid.NewGuid(); + var expected = new Order { Id = orderId }; + _repositorySubstitute + .FindAsync(orderId, Arg.Any()) + .Returns(Task.FromResult(expected)); + + // Act + var result = await _sut.GetOrderAsync(orderId); + + // Assert + result.ShouldBeEquivalentTo(expected); + } + + [Fact] + public async Task GetOrderAsync_WhenOrderNotFound_ThrowsNotFoundException() + { + // Arrange + var orderId = Guid.NewGuid(); + _repositorySubstitute + .FindAsync(orderId, Arg.Any()) + .Returns(Task.FromResult(null)); + + // Act + var act = () => _sut.GetOrderAsync(orderId); + + // Assert + await act.ShouldThrowAsync(); + } + + [Fact] + public async Task SaveOrderAsync_CallsRepository_VerifiesInteraction() + { + // Arrange + var order = new Order { Id = Guid.NewGuid() }; + + // Act + await _sut.SaveOrderAsync(order); + + // Assert + await _repositorySubstitute.Received(1).SaveAsync(order, Arg.Any()); + } +} +``` + +**Why NSubstitute?** +- ✅ Fluent, readable syntax — substitutes ARE the interfaces, no `.Object` indirection +- ✅ Lower ceremony, less noise in tests +- ✅ `Arg.Any()` is cleaner than `It.IsAny()` +- ✅ `Received()` reads naturally as "verify it received this call" +- ✅ Faster to write and understand, even for complex scenarios + +### 10.2 Parameterized Tests + +```csharp +// ✅ GOOD - Theory with InlineData for multiple cases +[Theory] +[InlineData(CustomerTier.Bronze, 0.00)] +[InlineData(CustomerTier.Silver, 0.05)] +[InlineData(CustomerTier.Gold, 0.10)] +[InlineData(CustomerTier.Platinum, 0.20)] +public void GetDiscount_ReturnsCorrectRate(CustomerTier tier, decimal expected) +{ + var result = _sut.GetDiscount(tier); + result.ShouldBe(expected); +} + +// ✅ GOOD - MemberData for complex input objects +public static IEnumerable InvalidOrders => +[ + [new Order { Items = [] }, "Order must contain at least one item."], + [new Order { Items = [item], TotalAmount = -1 }, "Order total must be greater than zero."], +]; + +[Theory] +[MemberData(nameof(InvalidOrders))] +public void PlaceOrder_WithInvalidOrder_ThrowsValidationException(Order order, string expectedError) +{ + var act = () => _sut.PlaceOrder(order, Guid.NewGuid()); + act.ShouldThrow() + .Errors.ShouldContain(expectedError); +} +``` + +### 10.3 Test Naming + +```csharp +// ✅ GOOD - MethodName_StateUnderTest_ExpectedBehavior +public async Task GetOrderAsync_WhenOrderExists_ReturnsOrder() { } +public async Task GetOrderAsync_WhenOrderNotFound_ThrowsNotFoundException() { } +public void PlaceOrder_WithEmptyCart_ThrowsValidationException() { } +public void CalculateDiscount_ForPlatinumCustomer_Returns20Percent() { } +``` + +### 10.4 Avoid Logic in Tests + +```csharp +// ✅ GOOD - Direct, no conditionals or loops +[Fact] +public void FormatName_ReturnsFullName() +{ + var user = new User { FirstName = "John", LastName = "Doe" }; + var result = _sut.FormatName(user); + result.ShouldBe("John Doe"); +} + +// ❌ AVOID - Logic in tests (makes failures hard to diagnose) +[Fact] +public void FormatNames_ReturnsFullNames() +{ + var users = GetTestUsers(); + foreach (var user in users) + { + if (user.FirstName != null) + _sut.FormatName(user).ShouldContain(user.FirstName); + } +} +``` + +### 10.5 Integration Tests + +```csharp +// ✅ GOOD - Use WebApplicationFactory for ASP.NET Core integration tests +public class OrdersApiTests : IClassFixture> +{ + private readonly HttpClient _client; + + public OrdersApiTests(WebApplicationFactory factory) + { + _client = factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + // Replace real DB with in-memory for tests + services.RemoveAll(); + services.AddDbContext(o => o.UseInMemoryDatabase("TestDb")); + }); + }).CreateClient(); + } + + [Fact] + public async Task GetOrder_ReturnsOk() + { + var response = await _client.GetAsync("/api/orders/123"); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } +} +``` + +--- + +## Quick Reference + +| Topic | Key Rule | +|-------|---------| +| Naming | PascalCase methods/properties, `_camelCase` fields, `I`-prefix interfaces, `Async` suffix | +| Nullability | Enable `enable`, use `?` explicitly, guard with `ArgumentNullException.ThrowIfNull` | +| Async | Always pass `CancellationToken`, avoid `async void`, prefer `Task.WhenAll` for parallel ops | +| LINQ | Materialize with `ToList()` once, use `Any()` not `Count() > 0` | +| Errors | Specific exception types, validate at entry, `Result` for expected failures | +| Pattern matching | Switch expressions over switch statements, property patterns for readable conditionals | +| Organization | File-scoped namespaces, feature-based folders, one type per file | +| Immutability | Records for value objects/DTOs, `IReadOnlyList` for exposed collections | +| DI | Constructor injection, `readonly` fields, Options pattern for config | +| Testing | xUnit + Shouldly, `[Fact]`/`[Theory]`, Arrange-Act-Assert, no logic in tests | diff --git a/opencode/context/core/standards/typescript.md b/opencode/context/core/standards/typescript.md new file mode 100644 index 0000000..4c49e90 --- /dev/null +++ b/opencode/context/core/standards/typescript.md @@ -0,0 +1,624 @@ + + +# Universal TypeScript Standards + +**Purpose**: Universal TypeScript patterns applicable to any TypeScript project +**Scope**: Language-level patterns, not framework-specific +**Last Updated**: 2026-02-16 + +--- + +## Table of Contents + +1. [Function Patterns](#1-function-patterns) +2. [Type Safety](#2-type-safety) +3. [Array Operations](#3-array-operations) +4. [Async Patterns](#4-async-patterns) +5. [Control Flow](#5-control-flow) +6. [Code Organization](#6-code-organization) +7. [Testing Principles](#7-testing-principles) +8. [Variable Naming](#8-variable-naming) + +--- + +## 1. Function Patterns + +### 1.1 Naming Convention + +**Rule: Prefer single-word function names** + +```typescript +// ✅ GOOD - Single-word names +export function create() {...} +export function fork() {...} +export function touch() {...} +export function get() {...} +export async function stream(input: StreamInput) {...} + +// ✅ ACCEPTABLE - Multi-word only when necessary +export function isDefaultTitle(title: string) {...} // Boolean predicate +export function assertNotBusy(sessionID: string) {...} // Assertion pattern +export async function createNext(input) {...} // Version disambiguation +export async function resolvePromptParts(template) {...} // Complex operation needs clarity + +// ❌ AVOID - Unnecessary multi-word names +function prepareJournal(dir: string) {} // Use: journal() +function getUserData(id: string) {} // Use: user() +function processFileContent(path) {} // Use: process() +``` + +### 1.2 Pure Functions + +**Rule: Prefer pure functions when possible** + +```typescript +// ✅ GOOD - Pure function +function calculateTotal(items: Item[]): number { + return items.reduce((sum, item) => sum + item.price, 0) +} + +// ❌ AVOID - Side effects +let total = 0 +function addToTotal(item: Item) { + total += item.price // Mutates external state +} +``` + +### 1.3 Function Composition + +```typescript +// ✅ GOOD - Functional composition with pipes +const filtered = agents + .filter((a) => a.mode !== "primary") + .filter((a) => hasPermission(a, caller)) + .map((a) => a.name) + +// ✅ GOOD - Higher-order functions +export function withRetry(fn: () => Promise, maxRetries: number): Promise { + return fn().catch((error) => { + if (maxRetries > 0) { + return withRetry(fn, maxRetries - 1) + } + throw error + }) +} +``` + +--- + +## 2. Type Safety + +### 2.1 TypeScript Types + +**Rule: Use TypeScript's type system, avoid `any`** + +```typescript +// ✅ GOOD - Explicit types +interface User { + id: string + name: string + email: string +} + +function getUser(id: string): User { + // Implementation +} + +// ❌ AVOID - any type +function getUser(id: any): any { + // Loses all type safety +} +``` + +### 2.2 Type Inference + +**Rule: Let TypeScript infer when obvious** + +```typescript +// ✅ GOOD - Inference works +const count = 42 // TypeScript knows this is number +const users = await fetchUsers() // Type inferred from return type + +// ❌ AVOID - Redundant annotations +const count: number = 42 +const users: User[] = await fetchUsers() +``` + +### 2.3 Type Guards + +**Rule: Use type guards for runtime type checking** + +```typescript +// ✅ GOOD - Type guard +function isUser(value: unknown): value is User { + return ( + typeof value === "object" && + value !== null && + "id" in value && + "name" in value + ) +} + +// Usage +if (isUser(data)) { + console.log(data.name) // TypeScript knows data is User +} +``` + +### 2.4 Avoid Any + +**Rule: Use `unknown` instead of `any` when type is truly unknown** + +```typescript +// ✅ GOOD - unknown requires type checking +function processData(data: unknown) { + if (typeof data === "string") { + return data.toUpperCase() + } + throw new Error("Invalid data") +} + +// ❌ AVOID - any bypasses type checking +function processData(data: any) { + return data.toUpperCase() // No compile-time safety +} +``` + +--- + +## 3. Array Operations + +### 3.1 Functional Methods (Preferred) + +**Rule: Prefer map/filter/reduce over for-loops** + +```typescript +// ✅ GOOD - Functional chain with type inference +const files = messages + .flatMap((x) => x.parts) + .filter((x): x is Patch => x.type === "patch") + .flatMap((x) => x.files) + .map((x) => path.relative(worktree, x)) + +// ✅ GOOD - Parallel async operations +const results = await Promise.all( + toolCalls.map(async (call) => { + return executeCall(call) + }), +) + +// ✅ GOOD - Reduce for aggregation +const totalAdditions = diffs.reduce((sum, x) => sum + x.additions, 0) + +// ✅ GOOD - Unique values +const uniqueNames = Array.from(new Set(items.map((x) => x.name))) + +// ✅ GOOD - Sorting +const sorted = items.toSorted((a, b) => a.timestamp - b.timestamp) +``` + +### 3.2 For-Loops (When Necessary) + +**Rule: Use for-loops only for:** +1. Algorithm complexity (DP, graph traversal) +2. Early exit requirements +3. Sequential side effects +4. Performance-critical iteration + +```typescript +// ✅ GOOD - Early exit +const patches = [] +for (const msg of all) { + if (msg.info.id === targetID) break + for (const part of msg.parts) { + if (part.type === "patch") { + patches.push(part) + } + } +} + +// ✅ GOOD - Sequential mutations +for (const key of Object.keys(tools)) { + if (disabled.has(key)) { + delete tools[key] + } +} +``` + +### 3.3 Type Guards on Filter + +**Rule: Use type guards to maintain type inference downstream** + +```typescript +// ✅ GOOD - Type guard preserves type information +const patches = messages + .flatMap((msg) => msg.parts) + .filter((part): part is PatchPart => part.type === "patch") +// patches is now PatchPart[], not Part[] + +// ❌ BAD - Loses type information +const patches = messages + .flatMap((msg) => msg.parts) + .filter((part) => part.type === "patch") +// patches is still Part[], requires casting later +``` + +--- + +## 4. Async Patterns + +### 4.1 Parallel Execution (Default Pattern) + +**Rule: Use `Promise.all` for independent operations** + +```typescript +// ✅ GOOD - Parallel independent operations +const [language, cfg, provider, auth] = await Promise.all([ + getLanguage(model), + getConfig(), + getProvider(model.providerID), + getAuth(model.providerID), +]) + +// ✅ GOOD - Parallel array processing +const results = await Promise.all( + items.map(async (item) => { + return processItem(item) + }), +) + +// ❌ BAD - Sequential when independent +const language = await getLanguage(model) +const cfg = await getConfig() // Could run in parallel! +const provider = await getProvider(model.providerID) +``` + +### 4.2 Sequential Operations + +**Rule: Chain when operations depend on previous results** + +```typescript +// ✅ GOOD - Sequential dependency chain +const session = await createSession({ title: "New" }) +const message = await addMessage(session.id, { content: "Hello" }) +const response = await processMessage(message.id) + +// ✅ GOOD - Promise chain for clarity +const result = await createSession({ title: "New" }) + .then((session) => addMessage(session.id, { content: "Hello" })) + .then((message) => processMessage(message.id)) +``` + +### 4.3 Error Handling in Async + +**Rule: Prefer `.catch()` over try/catch when possible** + +```typescript +// ✅ GOOD - Catch at call site +const result = await operation().catch((error) => { + console.error("Operation failed", error) + return defaultValue +}) + +// ✅ GOOD - Promise.all with error handling +const results = await Promise.all( + items.map(async (item) => { + return processItem(item).catch((error) => { + console.error("Item failed", { item, error }) + return null + }) + }), +) + +// ✅ ACCEPTABLE - try/catch for multiple operations +try { + const session = await createSession(input) + await addMessage(session.id, message) + await publishEvent({ session }) + return session +} catch (error) { + console.error("Session creation failed", error) + throw error +} + +// ❌ AVOID - try/catch for single operation +try { + const result = await operation() + return result +} catch (error) { + console.error(error) + throw error +} +// Better: +const result = await operation().catch((error) => { + console.error(error) + throw error +}) +``` + +--- + +## 5. Control Flow + +### 5.1 Early Returns + +**Rule: Avoid `else` statements, use early returns** + +```typescript +// ✅ GOOD - Early returns +function getStatus(session: Session) { + if (!session) return "not_found" + if (session.busy) return "busy" + if (session.error) return "error" + return "ready" +} + +async function process(id: string) { + const session = await getSession(id) + if (!session) return { error: "Not found" } + + const result = await execute(session) + if (!result.success) return { error: result.message } + + return { data: result.data } +} + +// ❌ BAD - Else statements +function getStatus(session: Session) { + if (!session) { + return "not_found" + } else { + if (session.busy) { + return "busy" + } else { + if (session.error) { + return "error" + } else { + return "ready" + } + } + } +} +``` + +### 5.2 Guard Clauses + +```typescript +// ✅ GOOD - Guard clauses at function start +async function updateSession(id: string, data: UpdateData) { + if (!id) throw new Error("ID required") + if (!data) throw new Error("Data required") + if (data.title && data.title.length > 100) throw new Error("Title too long") + + // Main logic here + const session = await getSession(id) + await update(id, data) + return session +} +``` + +### 5.3 Switch Statements + +**Rule: Use exhaustive switch with default case** + +```typescript +// ✅ GOOD - Exhaustive switch +function handleEvent(event: Event) { + switch (event.type) { + case "start": + return handleStart(event) + + case "update": + return handleUpdate(event) + + case "complete": + return handleComplete(event) + + default: + const _exhaustive: never = event + throw new Error(`Unhandled event type: ${(event as any).type}`) + } +} +``` + +--- + +## 6. Code Organization + +### 6.1 Import Order + +**Rule: Organize imports by source** + +```typescript +// ✅ GOOD - Organized imports +// 1. Node built-ins +import path from "path" +import fs from "fs/promises" + +// 2. External packages +import { z } from "zod" +import express from "express" + +// 3. Internal modules +import { User } from "./types" +import { getConfig } from "./config" +``` + +### 6.2 Naming Conventions + +```typescript +// ✅ GOOD - Clear naming +const session = await getSession(id) +const user = await getCurrentUser() +const messages = await getMessages({ sessionID }) + +// ❌ BAD - Unnecessary verbosity +const currentSession = await getSession(id) +const currentlyAuthenticatedUser = await getCurrentUser() +const sessionMessagesList = await getMessages({ sessionID }) + +// ✅ GOOD - Multi-word when single word is ambiguous +const sessionID = params.id +const userAgent = req.headers["user-agent"] +const maxRetries = config.retries +``` + +### 6.3 File Structure + +**Rule: One primary export per file** + +```typescript +// user.ts +export interface User { + id: string + name: string +} + +export async function getUser(id: string): Promise { + // Implementation +} + +export async function createUser(data: CreateUserInput): Promise { + // Implementation +} +``` + +--- + +## 7. Testing Principles + +### 7.1 Test Structure + +**Rule: Follow Arrange-Act-Assert pattern** + +```typescript +// ✅ GOOD - AAA pattern +test("creates user with valid data", async () => { + // Arrange + const userData = { name: "Alice", email: "alice@example.com" } + + // Act + const user = await createUser(userData) + + // Assert + expect(user.name).toBe("Alice") + expect(user.email).toBe("alice@example.com") +}) +``` + +### 7.2 Coverage Goals + +**Rule: Test both success and failure cases** + +```typescript +// ✅ GOOD - Both positive and negative tests +describe("createUser", () => { + test("creates user with valid data", async () => { + const user = await createUser({ name: "Alice", email: "alice@example.com" }) + expect(user).toBeDefined() + }) + + test("throws error with invalid email", async () => { + await expect( + createUser({ name: "Alice", email: "invalid" }) + ).rejects.toThrow("Invalid email") + }) +}) +``` + +### 7.3 Mock External Dependencies + +**Rule: Mock all external dependencies** + +```typescript +// ✅ GOOD - Mocked dependencies +test("fetches user data", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + json: () => Promise.resolve({ id: "1", name: "Alice" }) + }) + + global.fetch = mockFetch + + const user = await fetchUser("1") + expect(user.name).toBe("Alice") +}) +``` + +--- + +## 8. Variable Naming + +### 8.1 Variable Declaration + +**Rule: Prefer `const` over `let`** + +```typescript +// ✅ GOOD - Immutable with ternary +const foo = condition ? 1 : 2 +const result = await (isValid ? processValid() : processInvalid()) + +// ❌ BAD - Reassignment +let foo +if (condition) { + foo = 1 +} else { + foo = 2 +} + +// ✅ GOOD - Early return instead of reassignment +function getValue(condition: boolean) { + if (condition) return 1 + return 2 +} + +// ✅ ACCEPTABLE - let when mutation is necessary +let accumulator = 0 +for (const item of items) { + accumulator += item.value +} +``` + +### 8.2 Destructuring + +**Rule: Avoid unnecessary destructuring, preserve context with dot notation** + +```typescript +// ✅ GOOD - Preserve context +function process(session: Session) { + console.log("processing", { id: session.id, title: session.title }) + return { + id: session.id, + status: session.status, + owner: session.owner + } +} + +// ❌ BAD - Loses context, harder to read +function process(session: Session) { + const { id, title, status, owner } = session + console.log("processing", { id, title }) + return { id, status, owner } +} + +// ✅ ACCEPTABLE - Destructuring when improving readability +function renderUser({ name, email, avatar }: User) { + return `
${name} (${email})
` +} + +// ✅ ACCEPTABLE - Destructuring array returns +const [language, cfg, provider] = await Promise.all([...]) +``` + +--- + +## Related Standards + +- **OpenCode TypeScript**: `/home/pnx/.config/opencode/context/openagents-repo/standards/opencode-typescript.md` (framework-specific patterns) +- **Code Quality**: `/home/pnx/.config/opencode/context/core/standards/code-quality.md` (general quality standards) +- **Test Coverage**: `/home/pnx/.config/opencode/context/core/standards/test-coverage.md` (testing standards) + +--- + +**Version**: 1.0.0 +**Last Updated**: 2026-02-16 +**Maintainer**: OpenAgents Control Team diff --git a/opencode/opencode.json b/opencode/opencode.json index 599fab5..59c87bf 100644 --- a/opencode/opencode.json +++ b/opencode/opencode.json @@ -15,11 +15,30 @@ "color": "secondary", }, }, - "mcp": { - "linear": { - "type": "remote", - "url": "https://mcp.linear.app/mcp", - "enabled": true + "mcp": { }, + "permission": { + "bash": { + "*": "ask", + "php": "allow", + "php *": "allow", + "cat": "allow", + "cat *": "allow", + "ls": "allow", + "ls *": "allow", + "git": "allow", + "git *": "allow", + "git push": "deny", + "git push *": "deny" + }, + "read": { + "*": "ask", + "~/.config/opencode": "allow", + "~/.config/opencode/**": "allow" + }, + "external_directory": { + "*": "ask", + "~/.config/opencode": "allow", + "~/.config/opencode/**": "allow" } } } diff --git a/opencode/skills/task-management/router.sh b/opencode/skills/task-management/router.sh index 712c952..e52a813 100644 --- a/opencode/skills/task-management/router.sh +++ b/opencode/skills/task-management/router.sh @@ -72,14 +72,14 @@ find_project_root() { local dir dir="$(pwd)" while [ "$dir" != "/" ]; do - if [ -d "$dir/.git" ] || [ -f "$dir/package.json" ]; then + if [ -e "$dir/.git" ] || [ -f "$dir/package.json" ]; then echo "$dir" return 0 fi dir="$(dirname "$dir")" done pwd - return 1 + return 0 } # Handle help diff --git a/opencode/skills/task-management/scripts/task-cli.ts b/opencode/skills/task-management/scripts/task-cli.ts index 64b4068..e8c05e9 100644 --- a/opencode/skills/task-management/scripts/task-cli.ts +++ b/opencode/skills/task-management/scripts/task-cli.ts @@ -1,4 +1,5 @@ #!/usr/bin/env npx ts-node +/// /** * Task Management CLI * @@ -19,8 +20,8 @@ * .tmp/tasks/completed/{feature-slug}/ */ -const fs = require('fs'); -const path = require('path'); +import * as fs from "node:fs"; +import * as path from "node:path"; // Find project root (look for .git or package.json) function findProjectRoot(): string {