⚠️ IMPORTANT: This is a quick reference guide. The authoritative source of truth is.specify/memory/constitution.md. On any conflict, the constitution wins.
Ten Second Tom is a modern CLI application built with C# and .NET 10, following Vertical Slice Architecture (VSA) with strict adherence to the principles defined in the constitution.
- Constitution (READ FIRST):
.specify/memory/constitution.md - Architecture Tests:
tests/TenSecondTom.Tests/Architecture/VsaComplianceTests.cs
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.2 + FluentAssertions 8.7.1
Platforms: macOS, Windows, (Linux future)
- ✅ Read the constitution at
.specify/memory/constitution.md - ✅ Check existing tests - understand current behavior
- ✅ Understand the feature - read related files in the vertical slice
- ✅ Look for duplication - refactor rather than duplicate
One use case = One file with nested types:
// src/Features/Users/CreateUser.cs
namespace TenSecondTom.Features.Users;
/// <summary>
/// Creates a new user with validation and persistence.
/// </summary>
public static class CreateUser
{
public sealed record Command(string Username, string Email)
: IRequest<Result<Guid>>;
public sealed class Validator : AbstractValidator<Command>
{
public Validator()
{
RuleFor(x => x.Username).NotEmpty().MinimumLength(3);
RuleFor(x => x.Email).NotEmpty().EmailAddress();
}
}
public sealed class Handler(
IUserRepository repository,
ILogger<Handler> logger)
: IRequestHandler<Command, Result<Guid>>
{
public async Task<Result<Guid>> Handle(
Command request,
CancellationToken cancellationToken)
{
// Business logic here
// Validation already done by FluentValidation pipeline
// Logging already done by RequestLoggingPipelineBehavior
}
}
}File naming: [Verb][Noun].cs (e.g., CreateUser.cs, ListTemplates.cs, GenerateOutput.cs)
Never access IConfiguration directly. Always use strongly-typed options:
// 1. Create Options class in src/Shared/Options/
namespace TenSecondTom.Shared.Options;
public sealed class MyFeatureOptions
{
public const string SectionName = "TenSecondTom:MyFeature";
public required string ApiKey { get; init; }
public int Timeout { get; init; } = 30;
}
// 2. Create Validator in src/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> into your service
public sealed class MyService(IOptions<MyFeatureOptions> options)
{
private readonly MyFeatureOptions _options = options.Value;
public void DoWork()
{
var apiKey = _options.ApiKey; // ✅ Type-safe!
}
}Use IConfigurationSectionStore for reading/writing config:
// Read a section
var result = await sectionStore.ReadSectionAsync<AudioOptions>(
"TenSecondTom:Audio",
cancellationToken);
// Write a section
var writeResult = await sectionStore.WriteSectionAsync(
"TenSecondTom:Audio",
audioOptions,
cancellationToken);// ✅ File-scoped namespaces
namespace TenSecondTom.Features.Users;
// ✅ Primary constructors
public sealed class UserService(
IUserRepository repository,
ILogger<UserService> logger)
{
// Use repository and logger directly
}
// ✅ Records for DTOs
public sealed record UserDto(Guid Id, string Username, string Email);
// ✅ Required properties
public sealed class UserConfig
{
public required string ConnectionString { get; init; }
}
// ✅ Collection expressions
var users = [user1, user2, user3];
// ✅ Constants for shared strings (NO magic strings!)
var dir = configuration[ConfigurationKeys.RootDirectory];
if (command == CommandNames.Today) { /* ... */ }- Write test showing expected behavior
- Verify RED - test fails with clear message
- Minimal code to make test pass
- Verify GREEN - test passes
- Refactor while keeping tests green
public sealed class CreateUserTests
{
[Fact]
public async Task Handle_WithValidCommand_CreatesUser()
{
// Arrange
var repository = new Mock<IUserRepository>();
var logger = Mock.Of<ILogger<CreateUser.Handler>>();
var handler = new CreateUser.Handler(repository.Object, logger);
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();
}
}Coverage Requirement: 80% minimum across all features
- Features use MediatR to call other features:
await _mediator.Send(new OtherFeature.Query()) - Infrastructure coordinates features (no business logic)
- Shared code in
src/Shared/(models, abstractions, constants)
- Features directly referencing other features
- God Objects (monolithic config/service classes)
- Magic strings (use constants from
Shared/Constants/) - Direct
IConfigurationaccess (use Options Pattern) [Obsolete]code in production (delete or refactor it)
public 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");
}
}// ✅ Correct: Use MediatR
var audioConfig = await _mediator.Send(new GetAudioConfiguration.Query());
if (audioConfig.IsSuccess)
{
var sttProvider = audioConfig.Value.SttProvider;
}
// ❌ Wrong: Direct feature reference
var audioService = new AudioService(); // NO! Cross-feature couplingInfrastructure: OS native notifications (macOS via osascript, Windows future)
// Send a basic notification
await _mediator.Send(new ShowNotification.Command
{
Title = "Recording Saved",
Message = "Your recording has been saved successfully",
Priority = NotificationPriority.Normal
});
// Send with timeout
await _mediator.Send(new ShowNotification.Command
{
Title = "Session Expiring",
Message = "Your 30-minute session has ended",
Priority = NotificationPriority.High,
TimeoutSeconds = 30
});Key Principles:
- Graceful degradation - notifications are enhancements, not requirements
- Non-blocking - use fire-and-forget pattern (Task.Run) to avoid blocking primary flows
- Error handling - catch and log notification failures, never propagate to calling code
- macOS limitations - no interactive buttons (AppleScript limitation)
- Terminal first - always preserve terminal output as primary interface
// Fire-and-forget pattern for notifications
_ = Task.Run(async () =>
{
try
{
await _mediator.Send(new ShowNotification.Command { /* ... */ });
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Notification failed (non-critical)");
}
});
// Continue with primary flow immediately
Console.WriteLine("Recording saved successfully");Configuration:
TenSecondTom:Notifications- Enable/disable, timeout, priority defaultsTenSecondTom:Security- Notification token secret, max token age
// 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 CreateUserCommand(username));
if (result.IsSuccess)
{
Console.WriteLine($"User created: {result.Value}");
return 0; // Success
}
Console.Error.WriteLine($"Error: {result.Error}");
return 1; // Failure
}, usernameOption);| Type | Convention | Example |
|---|---|---|
| Use Case Files | [Verb][Noun].cs |
CreateUser.cs, ListTemplates.cs |
| Nested Command/Query | Command or Query |
public sealed record Command(...) |
| Nested Validator | Validator |
public sealed class Validator |
| Nested Handler | Handler |
public sealed class Handler |
| Options Classes | [Feature]Options |
AudioOptions, LlmOptions |
| Options Validators | [Options]Validator |
AudioOptionsValidator |
| DI Methods | Add[Feature]Feature |
AddAuthFeature() |
| Test Files | [UseCase]Tests.cs |
CreateUserTests.cs |
// ❌ Don't Do
var config = _configuration["TenSecondTom:ApiKey"]; // Magic string
var handler = new OtherFeature.Handler(); // Cross-feature coupling
if (command == "today") { } // Magic string
// ✅ Do Instead
var config = _options.Value.ApiKey; // Options Pattern
var result = await _mediator.Send(new OtherFeature.Query()); // CQRS
if (command == CommandNames.Today) { } // Constant- Correctness - Code must work and handle edge cases
- Tests - TDD with 80% coverage
- Maintainability - DRY, clear, well-organized
- Performance - Optimize when justified
- Documentation - XML comments on public APIs
- Check
.specify/memory/constitution.mdfirst - Look for similar patterns in the codebase
- Run architecture tests:
tests/TenSecondTom.Tests/Architecture/VsaComplianceTests.cs - Ask the user for clarification
Constitution Version: 1.8.0 | Last Updated: 2025-01-19
Recent Changes:
- OS Native Notification System added (macOS support, Windows future)
- NotificationService infrastructure with channel-agnostic architecture
- Recording session expiration notifications (graceful degradation)
- 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