⚠️ IMPORTANT: This is a quick reference guide for AI agents. The authoritative source of truth is.specify/memory/constitution.md. On any conflict, the constitution wins.
- Constitution (READ FIRST):
.specify/memory/constitution.md - Architecture Tests:
tests/TenSecondTom.Tests/Architecture/VsaComplianceTests.cs
Ten Second Tom is a modern CLI application built with C# and .NET 10, following Vertical Slice Architecture (VSA) with the Co-location Pattern. All architectural principles are defined in the constitution.
Language: C# 14 with .NET 10
CLI: System.CommandLine 2.0-rc
UI: Spectre.Console 0.51.1
CQRS: MediatR 13.1.0
Validation: FluentValidation 12.0.0
Logging: Serilog 4.3.0
Testing: xUnit 2.9+ + FluentAssertions 8.7+
Platforms: macOS, Windows, (Linux future)
- ✅ Read the constitution at
.specify/memory/constitution.md - ✅ Check existing tests and understand current behavior
- ✅ Locate the feature slice - all related code should be nearby
- ✅ Look for duplication - refactor rather than duplicate
- Write Test - Create test showing expected behavior
- Verify Red - Confirm test fails with clear error
- Minimal Implementation - Write just enough code to pass
- Verify Green - Confirm test passes
- Refactor - Clean up code while keeping tests green
One use case = One file with nested Command/Query, Validator, Handler:
// src/Features/[FeatureName]/[UseCase].cs
namespace TenSecondTom.Features.[FeatureName];
/// <summary>Brief description of use case</summary>
public static class [UseCase]
{
public sealed record Command(...) : IRequest<Result<T>>;
public sealed class Validator : AbstractValidator<Command>
{
public Validator()
{
// Validation rules (auto-discovered)
}
}
public sealed class Handler(...) : IRequestHandler<Command, Result<T>>
{
public async Task<Result<T>> Handle(Command request, CancellationToken ct)
{
// Business logic (validation/logging already done by pipeline)
}
}
}File naming: [Verb][Noun].cs (e.g., CreateUser.cs, ListTemplates.cs, GenerateOutput.cs)
Never access IConfiguration directly. Always use the Options Pattern:
// ❌ PROHIBITED
var apiKey = _configuration["TenSecondTom:ApiKey"];
// ✅ REQUIRED
// 1. Options class in Shared/Options/
public sealed class MyFeatureOptions
{
public const string SectionName = "TenSecondTom:MyFeature";
public required string ApiKey { get; init; }
}
// 2. Validator in Shared/Options/Validation/
public sealed class MyFeatureOptionsValidator : IValidateOptions<MyFeatureOptions>
{
public ValidateOptionsResult Validate(string? name, MyFeatureOptions options)
{
if (string.IsNullOrWhiteSpace(options.ApiKey))
return ValidateOptionsResult.Fail("ApiKey is required");
return ValidateOptionsResult.Success;
}
}
// 3. Register in ServiceCollectionExtensions.cs
services.Configure<MyFeatureOptions>(configuration.GetSection(MyFeatureOptions.SectionName));
services.AddSingleton<IValidateOptions<MyFeatureOptions>, MyFeatureOptionsValidator>();
// 4. Inject IOptions<T>
public sealed class MyService(IOptions<MyFeatureOptions> options)
{
private readonly MyFeatureOptions _options = options.Value;
}// ✅ File-scoped namespaces
namespace TenSecondTom.Features.Users;
// ✅ Primary constructors
public sealed class UserService(IUserRepository repo, ILogger<UserService> logger)
{
// Use dependencies directly
}
// ✅ Records for DTOs
public sealed record UserDto(Guid Id, string Username, string Email);
// ✅ Required properties
public sealed class Config
{
public required string ApiKey { get; init; }
}
// ✅ Collection expressions
var items = [item1, item2, item3];
// ✅ Constants (NO magic strings!)
var dir = configuration[ConfigurationKeys.RootDirectory];
if (command == CommandNames.Today) { }// Test structure: Arrange-Act-Assert
public sealed class CreateUserTests
{
[Fact]
public async Task Handle_WithValidCommand_CreatesUser()
{
// Arrange
var repository = new Mock<IUserRepository>();
var handler = new CreateUser.Handler(
repository.Object,
Mock.Of<ILogger<CreateUser.Handler>>());
var command = new CreateUser.Command("john", "john@example.com");
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
result.IsSuccess.Should().BeTrue();
result.Value.Should().NotBeEmpty();
}
[Theory]
[InlineData("", "test@example.com")]
[InlineData("john", "")]
public async Task Validator_WithInvalidInput_Fails(string username, string email)
{
// Arrange
var validator = new CreateUser.Validator();
var command = new CreateUser.Command(username, email);
// Act
var result = await validator.ValidateAsync(command);
// Assert
result.IsValid.Should().BeFalse();
}
}// Cross-feature communication via MediatR
var audioConfig = await _mediator.Send(new GetAudioConfiguration.Query());
// Shared code in Shared/ directory
var logsDir = Path.Combine(baseDir, DirectoryNames.Logs);// Direct feature reference
var audioService = new AudioService(); // NO! Cross-feature coupling
// Magic strings
var key = config["TenSecondTom:ApiKey"]; // NO! Use Options Pattern
if (command == "today") { } // NO! Use CommandNames.Today
// God Objects
public class ConfigurationSettings { /* all app config */ } // NO! Violates VSA
// Obsolete code
[Obsolete] public void OldMethod() { } // NO! Delete it insteadpublic async Task<Result<User>> CreateUserAsync(string username)
{
if (string.IsNullOrWhiteSpace(username))
return Result<User>.Failure("Username is required");
try
{
var user = await _repository.CreateAsync(username);
return Result<User>.Success(user);
}
catch (DuplicateUserException ex)
{
_logger.LogWarning(ex, "Duplicate user: {Username}", username);
return Result<User>.Failure($"User {username} already exists");
}
}| Type | Convention | Example |
|---|---|---|
| Use Case Files | [Verb][Noun].cs |
CreateUser.cs, GenerateOutput.cs |
| Nested Types | Command, Query, Validator, Handler |
No prefixes |
| Options Classes | [Feature]Options |
AudioOptions, LlmOptions |
| Options Validators | [Options]Validator |
AudioOptionsValidator |
| Constants | [Domain]Keys/Names/Constants |
ConfigurationKeys, CommandNames |
| DI Methods | Add[Feature]Feature |
AddAuthFeature(), AddTemplatesFeature() |
| Test Files | [UseCase]Tests.cs |
CreateUserTests.cs |
// Use System.CommandLine
var createCommand = new Command("create", "Create a new user");
var usernameOption = new Option<string>("--username") { IsRequired = true };
createCommand.AddOption(usernameOption);
createCommand.SetHandler(async (string username) =>
{
var result = await _mediator.Send(new CreateUser.Command(username, email));
if (result.IsSuccess)
{
Console.WriteLine($"User created: {result.Value}");
return 0; // Success
}
Console.Error.WriteLine($"Error: {result.Error}");
return 1; // Failure
}, usernameOption);- Correctness - Code must work correctly and handle edge cases
- Tests - TDD with 80% coverage (non-negotiable)
- Maintainability - DRY, clear, well-organized code
- Performance - Optimize when justified
- Documentation - XML comments on public APIs
❌ Web frameworks (ASP.NET Core, Blazor)
❌ GUI frameworks (WPF, WinForms)
❌ Direct IConfiguration access with strings
❌ Magic strings for config keys, commands, paths
❌ Cross-feature coupling (direct references)
❌ God Objects (monolithic classes)
❌ [Obsolete] attributes (delete old code)
❌ Code without tests
❌ Test frameworks other than xUnit
❌ Ignoring 80% coverage requirement
- Check
.specify/memory/constitution.mdfirst - Look for similar patterns in the codebase
- Run architecture tests to verify VSA compliance:
tests/TenSecondTom.Tests/Architecture/VsaComplianceTests.cs - Ask the user for clarification
Constitution Version: 1.8.0 | Last Updated: 2025-01-19
Recent Changes:
- ConfigurationSettings God Object removed (aggressive refactor complete)
- IConfigurationSectionStore is now the standard for config storage
- Force parameter pattern for independent configuration commands
- All
[Obsolete]code removed from production
For detailed architectural principles, design patterns, and governance, consult .specify/memory/constitution.md.