ten-second-tom CLAUDE.md
> **⚠️ IMPORTANT**: This is a quick reference guide. The **authoritative source of truth** is [`.specify/memory/constitution.md`](./.specify/memory/constitution.md). On any conflict, the constitution wins.
Ten Second Tom - Claude Code Quick Reference
⚠️ IMPORTANT: This is a quick reference guide. The authoritative source of truth is
.specify/memory/constitution.md. On any conflict, the constitution wins.
Project Overview
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.
Quick Links
- Constitution (READ FIRST):
.specify/memory/constitution.md - Architecture Tests:
tests/TenSecondTom.Tests/Architecture/VsaComplianceTests.cs
Core Technology Stack
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)
Before Making Changes
- ✅ 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
Code Organization (Co-location Pattern)
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)
Configuration Management
✅ REQUIRED: Options Pattern
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!
}
}
Configuration Storage
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);
Modern C# Features (Required)
// ✅ 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) { /* ... */ }
Testing (TDD - Non-Negotiable)
Red-Green-Refactor Cycle
- 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
Test Structure (AAA Pattern)
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
VSA Compliance Rules
✅ Allowed
- 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)
❌ Prohibited
- 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)
Common Patterns
Result Pattern (Error Handling)
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");
}
}
CQRS Cross-Feature Communication
// ✅ 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 coupling
Notification System
Infrastructure: 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
CLI Command Structure
// 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);
Naming Conventions
| 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 |
What to Avoid
// ❌ 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
Priority Order
- 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
When in Doubt
- 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