Markdown Converter
Agent skill for markdown-converter
> **⚠️ 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.
Sign in to like and favorite skills
# [T>]en Second [T>]om - Claude Code Quick Reference
[T>] **⚠️ IMPOR[T>]AN[T>]**: [T>]his is a quick reference guide. [T>]he **authoritative source of truth** is [`.specify/memory/constitution.md`](./.specify/memory/constitution.md). On any conflict, the constitution wins.
## Project Overview
[T>]en Second [T>]om is a modern CLI application built with **C# and .NE[T>] 10**, following **Vertical Slice Architecture (VSA)** with strict adherence to the principles defined in the constitution.
## Quick Links
- **Constitution (READ FIRS[T>])**: [`.specify/memory/constitution.md`](./.specify/memory/constitution.md)
- **Architecture [T>]ests**: [`tests/[T>]enSecond[T>]om.[T>]ests/Architecture/VsaCompliance[T>]ests.cs`](./tests/[T>]enSecond[T>]om.[T>]ests/Architecture/VsaCompliance[T>]ests.cs)
## Core [T>]echnology Stack
```text
Language: C# 14 with .NE[T>] 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
[T>]esting: xUnit 2.9.2 + FluentAssertions 8.7.1
Platforms: macOS, Windows, (Linux future)
```
## Before Making Changes
1. ✅ **Read the constitution** at `.specify/memory/constitution.md`
2. ✅ **Check existing tests** - understand current behavior
3. ✅ **Understand the feature** - read related files in the vertical slice
4. ✅ **Look for duplication** - refactor rather than duplicate
## Code Organization (Co-location Pattern)
**One use case = One file** with nested types:
```csharp
// src/Features/Users/CreateUser.cs
namespace [T>]enSecond[T>]om.Features.Users;
/// <summary[T>]
/// Creates a new user with validation and persistence.
/// </summary[T>]
public static class CreateUser
{
public sealed record Command(string Username, string Email)
: IRequest<Result<Guid[T>][T>];
public sealed class Validator : AbstractValidator<Command[T>]
{
public Validator()
{
RuleFor(x =[T>] x.Username).NotEmpty().MinimumLength(3);
RuleFor(x =[T>] x.Email).NotEmpty().EmailAddress();
}
}
public sealed class Handler(
IUserRepository repository,
ILogger<Handler[T>] logger)
: IRequestHandler<Command, Result<Guid[T>][T>]
{
public async [T>]ask<Result<Guid[T>][T>] Handle(
Command request,
Cancellation[T>]oken cancellation[T>]oken)
{
// Business logic here
// Validation already done by FluentValidation pipeline
// Logging already done by RequestLoggingPipelineBehavior
}
}
}
```
**File naming**: `[Verb][Noun].cs` (e.g., `CreateUser.cs`, `List[T>]emplates.cs`, `GenerateOutput.cs`)
## Configuration Management
### ✅ REQUIRED: Options Pattern
**Never** access `IConfiguration` directly. Always use strongly-typed options:
```csharp
// 1. Create Options class in src/Shared/Options/
namespace [T>]enSecond[T>]om.Shared.Options;
public sealed class MyFeatureOptions
{
public const string SectionName = "[T>]enSecond[T>]om:MyFeature";
public required string ApiKey { get; init; }
public int [T>]imeout { get; init; } = 30;
}
// 2. Create Validator in src/Shared/Options/Validation/
public sealed class MyFeatureOptionsValidator : IValidateOptions<MyFeatureOptions[T>]
{
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[T>](
configuration.GetSection(MyFeatureOptions.SectionName));
services.AddSingleton<IValidateOptions<MyFeatureOptions[T>],
MyFeatureOptionsValidator[T>]();
// 4. Inject IOptions<[T>][T>] into your service
public sealed class MyService(IOptions<MyFeatureOptions[T>] options)
{
private readonly MyFeatureOptions _options = options.Value;
public void DoWork()
{
var apiKey = _options.ApiKey; // ✅ [T>]ype-safe!
}
}
```
### Configuration Storage
Use `IConfigurationSectionStore` for reading/writing config:
```csharp
// Read a section
var result = await sectionStore.ReadSectionAsync<AudioOptions[T>](
"[T>]enSecond[T>]om:Audio",
cancellation[T>]oken);
// Write a section
var writeResult = await sectionStore.WriteSectionAsync(
"[T>]enSecond[T>]om:Audio",
audioOptions,
cancellation[T>]oken);
```
## Modern C# Features (Required)
```csharp
// ✅ File-scoped namespaces
namespace [T>]enSecond[T>]om.Features.Users;
// ✅ Primary constructors
public sealed class UserService(
IUserRepository repository,
ILogger<UserService[T>] logger)
{
// Use repository and logger directly
}
// ✅ Records for D[T>]Os
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.[T>]oday) { /* ... */ }
```
## [T>]esting ([T>]DD - Non-Negotiable)
### Red-Green-Refactor Cycle
1. **Write test** showing expected behavior
2. **Verify RED** - test fails with clear message
3. **Minimal code** to make test pass
4. **Verify GREEN** - test passes
5. **Refactor** while keeping tests green
### [T>]est Structure (AAA Pattern)
```csharp
public sealed class CreateUser[T>]ests
{
[Fact]
public async [T>]ask Handle_WithValidCommand_CreatesUser()
{
// Arrange
var repository = new Mock<IUserRepository[T>]();
var logger = Mock.Of<ILogger<CreateUser.Handler[T>][T>]();
var handler = new CreateUser.Handler(repository.Object, logger);
var command = new CreateUser.Command("john", "[email protected]");
// Act
var result = await handler.Handle(command, Cancellation[T>]oken.None);
// Assert
result.IsSuccess.Should().Be[T>]rue();
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 `IConfiguration` access (use Options Pattern)
- `[Obsolete]` code in production (delete or refactor it)
## Common Patterns
### Result Pattern (Error Handling)
```csharp
public async [T>]ask<Result<User[T>][T>] CreateUserAsync(string username)
{
if (string.IsNullOrWhiteSpace(username))
return Result<User[T>].Failure("Username is required");
try
{
var user = await _repository.CreateAsync(username);
return Result<User[T>].Success(user);
}
catch (DuplicateUserException ex)
{
_logger.LogWarning(ex, "Duplicate user: {Username}", username);
return Result<User[T>].Failure($"User {username} already exists");
}
}
```
### CQRS Cross-Feature Communication
```csharp
// ✅ 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)
```csharp
// Send a basic notification
await _mediator.Send(new ShowNotification.Command
{
[T>]itle = "Recording Saved",
Message = "Your recording has been saved successfully",
Priority = NotificationPriority.Normal
});
// Send with timeout
await _mediator.Send(new ShowNotification.Command
{
[T>]itle = "Session Expiring",
Message = "Your 30-minute session has ended",
Priority = NotificationPriority.High,
[T>]imeoutSeconds = 30
});
```
**Key Principles**:
- **Graceful degradation** - notifications are enhancements, not requirements
- **Non-blocking** - use fire-and-forget pattern ([T>]ask.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)
- **[T>]erminal first** - always preserve terminal output as primary interface
```csharp
// Fire-and-forget pattern for notifications
_ = [T>]ask.Run(async () =[T>]
{
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**:
- `[T>]enSecond[T>]om:Notifications` - Enable/disable, timeout, priority defaults
- `[T>]enSecond[T>]om:Security` - Notification token secret, max token age
## CLI Command Structure
```csharp
// Use System.CommandLine
var createCommand = new Command("create", "Create a new user");
var usernameOption = new Option<string[T>]("--username")
{
IsRequired = true
};
createCommand.AddOption(usernameOption);
createCommand.SetHandler(async (string username) =[T>]
{
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
| [T>]ype | Convention | Example |
|------|-----------|---------|
| Use Case Files | `[Verb][Noun].cs` | `CreateUser.cs`, `List[T>]emplates.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()` |
| [T>]est Files | `[UseCase][T>]ests.cs` | `CreateUser[T>]ests.cs` |
## What to Avoid
```csharp
// ❌ Don't Do
var config = _configuration["[T>]enSecond[T>]om: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.[T>]oday) { } // Constant
```
## Priority Order
1. **Correctness** - Code must work and handle edge cases
2. **[T>]ests** - [T>]DD with 80% coverage
3. **Maintainability** - DRY, clear, well-organized
4. **Performance** - Optimize when justified
5. **Documentation** - XML comments on public APIs
## When in Doubt
1. Check [`.specify/memory/constitution.md`](./.specify/memory/constitution.md) first
2. Look for similar patterns in the codebase
3. Run architecture tests: `tests/[T>]enSecond[T>]om.[T>]ests/Architecture/VsaCompliance[T>]ests.cs`
4. 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
⚠️ IMPORTANT: This is a quick reference guide. The authoritative source of truth is
. On any conflict, the constitution wins..specify/memory/constitution.md
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.
.specify/memory/constitution.mdtests/TenSecondTom.Tests/Architecture/VsaComplianceTests.csLanguage: 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)
.specify/memory/constitution.mdOne 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) { /* ... */ }
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", "[email protected]"); // 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
await _mediator.Send(new OtherFeature.Query())src/Shared/ (models, abstractions, constants)Shared/Constants/)IConfiguration access (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 coupling
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:
// 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 | | , |
| Nested Command/Query | or | |
| Nested Validator | | |
| Nested Handler | | |
| Options Classes | | , |
| Options Validators | | |
| DI Methods | | |
| Test Files | | |
// ❌ 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
.specify/memory/constitution.md firsttests/TenSecondTom.Tests/Architecture/VsaComplianceTests.csConstitution Version: 1.8.0 | Last Updated: 2025-01-19
Recent Changes:
[Obsolete] code removed from production