Obsidian Storage Provider - Research & Proposed Architecture Plan
Establish a specification to build support for Obsidian and other storage providers, including migration of existing default storage into this new model.
Executive Summary
This document outlines the research, architectural design, and proposed constitutional amendments for adding pluggable storage provider support to Ten Second Tom, with Obsidian vault integration as the first alternative provider.
Important: This is a planning document. The constitution has NOT been amended yet. The proposed Principle IX is documented here for review and will be formally added to the constitution if this feature is approved for implementation.
Key Objectives
- Refactor current hard-coded file system storage into a provider-based architecture
- Introduce
IStorageProvider abstraction with automatic discovery via assembly scanning
- Implement two initial providers:
DefaultStorageProvider (current behavior, TST-native structure)
ObsidianStorageProvider (Obsidian vault-compatible structure)
- Extend setup wizard to allow users to select storage providers
- Enable future extensibility for additional providers (Notion, Joplin, LogSeq, etc.)
Research: Obsidian Vault Structure
Obsidian Vault Characteristics (from user screenshot)
CK/ # Vault root
├── .obsidian/ # App configuration (like TST's config/)
│ ├── app.json
│ ├── workspace.json
│ └── ...
├── 2025-10-28.md # Daily notes (root level)
├── app-ideas/ # Custom folders
│ ├── Photos.md
├── Recording_*.m4a # Media files co-located
│ └── ...
└── ten-second-tom/ # Example TST subfolder
├── Obsidian Integration.md
├── Security Ideas.md
├── Welcome.md
Key Insights:
- ✅ File-based Markdown: Just like TST (compatibility ✓)
- ✅ Flat or hierarchical: Supports both folder structures
- ✅ YAML frontmatter: Obsidian natively supports frontmatter (compatibility ✓)
- ✅ Automatic discovery: Files added outside Obsidian appear automatically
- ⚠️ Naming flexibility: Obsidian allows arbitrary file naming and organization
- 🔧 Bidirectional sync: Changes in either app reflect in both
TST Current Structure (from codebase analysis)
/Users/chris/ten-second-tom/ # Root (configurable via StorageOptions.MemoryDirectory)
├── config/
│ └── config.json # App configuration
├── templates/ # Prompt templates
│ ├── daily-entry.md
│ └── weekly-review.md
├── today/ # Daily entries
│ └── 2025/
│ └── 10/
│ └── today-10-28-2025-1.md
├── thisweek/ # Weekly entries
│ └── 2025/
│ └── week-43/
│ └── thisweek-week-43-2025-1.md
└── generate/ # Generated outputs
└── 2025/
└── 10/
└── 28/
└── generate-10-28-2025-1.md
Current Issues:
- ❌ MemoryDirectory is both root AND storage location (conflated concerns)
- ❌ config.json sits inside memory directory (configuration ⊂ data)
- ❌ Hard-coded in FileSystemStorageProvider (no abstraction)
- ❌ StorageOptions directly consumed (tight coupling)
Architectural Design
1. Storage Provider Abstraction
IStorageProvider Interface (extends IMemoryStorageProvider)
namespace TenSecondTom.Infrastructure.Storage;
/// <summary>
/// Defines a pluggable storage provider for memory entries.
/// Providers are automatically discovered via assembly scanning.
/// </summary>
public interface IStorageProvider : IMemoryStorageProvider
{
/// <summary>
/// Unique identifier for this storage provider (e.g., "default", "obsidian").
/// Used in configuration to select the active provider.
/// </summary>
string ProviderId { get; }
/// <summary>
/// Human-readable display name for the provider (e.g., "Default TST Storage", "Obsidian Vault").
/// </summary>
string DisplayName { get; }
/// <summary>
/// Description of the provider for user selection during setup.
/// </summary>
string Description { get; }
/// <summary>
/// Initialize the provider with configuration options.
/// Called after provider selection during setup.
/// </summary>
/// <param name="options">Storage configuration options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result indicating success or failure with error message.</returns>
Task<Result> InitializeAsync(StorageOptions options, CancellationToken cancellationToken);
/// <summary>
/// Validates the provider-specific configuration.
/// Called during setup to ensure the storage location is valid.
/// </summary>
/// <param name="options">Storage configuration options to validate.</param>
/// <returns>Result indicating whether configuration is valid.</returns>
Task<Result> ValidateConfigurationAsync(StorageOptions options);
}
Storage Provider Discovery (Assembly Scanning)
namespace TenSecondTom.Infrastructure.Storage;
/// <summary>
/// Factory for creating and managing storage providers.
/// Automatically discovers providers via assembly scanning.
/// </summary>
public sealed class StorageProviderFactory
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<StorageProviderFactory> _logger;
private readonly Dictionary<string, Type> _providers;
public StorageProviderFactory(IServiceProvider serviceProvider, ILogger<StorageProviderFactory> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
_providers = DiscoverProviders();
}
/// <summary>
/// Discovers all IStorageProvider implementations via assembly scanning.
/// </summary>
private Dictionary<string, Type> DiscoverProviders()
{
var providers = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
var assembly = typeof(ServiceCollectionExtensions).Assembly;
var providerTypes = assembly.GetTypes()
.Where(t => typeof(IStorageProvider).IsAssignableFrom(t)
&& !t.IsInterface
&& !t.IsAbstract)
.ToList();
foreach (var type in providerTypes)
{
// Instantiate to get ProviderId (providers must have parameterless constructor for discovery)
var instance = (IStorageProvider)Activator.CreateInstance(type)!;
providers[instance.ProviderId] = type;
_logger.LogInformation("Discovered storage provider: {ProviderId} ({TypeName})",
instance.ProviderId, type.Name);
}
return providers;
}
/// <summary>
/// Gets all available storage providers for user selection.
/// </summary>
public IReadOnlyList<IStorageProvider> GetAvailableProviders()
{
return _providers.Values
.Select(t => (IStorageProvider)ActivatorUtilities.CreateInstance(_serviceProvider, t))
.ToList();
}
/// <summary>
/// Creates a storage provider instance by ID.
/// </summary>
public IStorageProvider CreateProvider(string providerId)
{
if (!_providers.TryGetValue(providerId, out var providerType))
{
throw new InvalidOperationException($"Storage provider '{providerId}' not found.");
}
return (IStorageProvider)ActivatorUtilities.CreateInstance(_serviceProvider, providerType);
}
}
2. Configuration Refactoring
Current Problem
// ❌ Current: MemoryDirectory is conflated with storage root
public sealed class StorageOptions
{
public string MemoryDirectory { get; set; } = string.Empty; // Both root AND storage location but now called RootDirectory for clarity
public RetentionPolicy RetentionPolicy { get; set; }
// ...
}
Proposed Solution
namespace TenSecondTom.Shared.Options;
/// <summary>
/// Configuration options for storage providers and data persistence.
/// Maps to the "TenSecondTom:Storage" configuration section.
/// </summary>
public sealed class StorageOptions
{
public const string SectionName = "TenSecondTom:Storage";
/// <summary>
/// The selected storage provider ID (e.g., "default", "obsidian").
/// Determines which IStorageProvider implementation to use.
/// Default: "default" (TST-native storage).
/// </summary>
public string ProviderId { get; set; } = "default";
/// <summary>
/// Root directory for all TST data (config, templates, AND memory storage).
/// For Obsidian provider: path to Obsidian vault root
/// Environment variable: TenSecondTom__Storage__RootDirectory
/// </summary>
public string RootDirectory { get; set; } = string.Empty;
/// <summary>
/// Provider-specific subdirectory for memory entries (optional).
/// For default provider: "" (root level organization: today/, thisweek/, etc.)
/// For Obsidian provider: "ten-second-tom/" (subfolder in vault)
/// </summary>
public string? MemorySubdirectory { get; set; }
/// <summary>
/// Retention policy for memory entries.
/// </summary>
public RetentionPolicy RetentionPolicy { get; set; } = RetentionPolicy.Indefinite;
/// <summary>
/// Whether to automatically purge entries based on retention policy.
/// </summary>
public bool AutoPurge { get; set; }
/// <summary>
/// Optional maximum file size in bytes for memory entries.
/// </summary>
public long? MaxFileSizeBytes { get; set; }
/// <summary>
/// Whether to compress memory entries.
/// </summary>
public bool CompressionEnabled { get; set; }
}
3. Storage Provider Implementations
DefaultStorageProvider (TST-native)
namespace TenSecondTom.Infrastructure.Storage.Providers;
/// <summary>
/// Default Ten Second Tom storage provider using TST-native directory structure.
/// Maintains backward compatibility with existing installations.
/// </summary>
public sealed class DefaultStorageProvider : IStorageProvider
{
public string ProviderId => "default";
public string DisplayName => "Default TST Storage";
public string Description => "Ten Second Tom's native storage format with hierarchical date folders.";
private FileSystemStorageProvider? _implementation;
// IMemoryStorageProvider implementation delegates to FileSystemStorageProvider
// (existing implementation becomes the "default" provider's logic)
public async Task<Result> InitializeAsync(StorageOptions options, CancellationToken cancellationToken)
{
// Validate directory structure
string rootDir = options.RootDirectory;
if (!Directory.Exists(rootDir))
{
Directory.CreateDirectory(rootDir);
}
// Ensure subdirectories exist (today/, thisweek/, generate/, templates/, config/)
string[] subdirs = ["today", "thisweek", "generate", "templates", "config"];
foreach (var subdir in subdirs)
{
string path = Path.Combine(rootDir, subdir);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
_implementation = new FileSystemStorageProvider(rootDir, logger);
return Result.Success();
}
public Task<Result> ValidateConfigurationAsync(StorageOptions options)
{
// Validate that RootDirectory is writable
try
{
string testFile = Path.Combine(options.RootDirectory, ".write-test");
File.WriteAllText(testFile, "test");
File.Delete(testFile);
return Task.FromResult(Result.Success());
}
catch (Exception ex)
{
return Task.FromResult(Result.Failure($"Directory is not writable: {ex.Message}"));
}
}
}
ObsidianStorageProvider
namespace TenSecondTom.Infrastructure.Storage.Providers;
/// <summary>
/// Obsidian vault storage provider.
/// Stores memory entries in an Obsidian vault with optional subdirectory organization.
/// </summary>
public sealed class ObsidianStorageProvider : IStorageProvider
{
public string ProviderId => "obsidian";
public string DisplayName => "Obsidian Vault";
public string Description => "Store entries in an Obsidian vault for seamless note integration.";
public async Task<Result> InitializeAsync(StorageOptions options, CancellationToken cancellationToken)
{
string vaultRoot = options.RootDirectory;
// Validate Obsidian vault structure
string obsidianDir = Path.Combine(vaultRoot, ".obsidian");
if (!Directory.Exists(obsidianDir))
{
return Result.Failure($"Not a valid Obsidian vault: .obsidian directory not found at {vaultRoot}");
}
// Create TST subdirectory if specified
string memoryDir = string.IsNullOrEmpty(options.MemorySubdirectory)
? vaultRoot
: Path.Combine(vaultRoot, options.MemorySubdirectory);
if (!Directory.Exists(memoryDir))
{
Directory.CreateDirectory(memoryDir);
}
// Create organizational folders (Obsidian-friendly naming)
string[] subdirs = ["Daily Notes", "Weekly Reviews", "Generated"]; // More Obsidian-like naming
foreach (var subdir in subdirs)
{
string path = Path.Combine(memoryDir, subdir);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
return Result.Success();
}
public Task<Result> ValidateConfigurationAsync(StorageOptions options)
{
string vaultRoot = options.RootDirectory;
// Check if directory exists
if (!Directory.Exists(vaultRoot))
{
return Task.FromResult(Result.Failure($"Vault directory does not exist: {vaultRoot}"));
}
// Check for .obsidian directory
string obsidianDir = Path.Combine(vaultRoot, ".obsidian");
if (!Directory.Exists(obsidianDir))
{
return Task.FromResult(Result.Failure($"Not a valid Obsidian vault: .obsidian directory not found"));
}
// Check write permissions
try
{
string testFile = Path.Combine(vaultRoot, ".tst-write-test");
File.WriteAllText(testFile, "test");
File.Delete(testFile);
return Task.FromResult(Result.Success());
}
catch (Exception ex)
{
return Task.FromResult(Result.Failure($"Vault is not writable: {ex.Message}"));
}
}
// File path generation uses Obsidian-friendly structure
// E.g., "Daily Notes/2025-10-28 Entry 1.md" instead of "today/2025/10/today-10-28-2025-1.md"
}
4. Setup Wizard Integration
Updated SetupCommandHandler Flow
// Step 4: Storage Provider Selection (NEW)
_wizardUI.ShowStepHeader(4, 9, "Storage Provider Selection");
var availableProviders = _storageProviderFactory.GetAvailableProviders();
var selectedProvider = await _wizardUI.PromptForStorageProviderAsync(
availableProviders,
command.ExistingConfiguration?.Storage.ProviderId,
cancellationToken);
// Step 5: Storage Location Configuration (MODIFIED)
_wizardUI.ShowStepHeader(5, 9, $"{selectedProvider.DisplayName} Location");
string rootDirectory;
if (selectedProvider.ProviderId == "obsidian")
{
rootDirectory = await _wizardUI.PromptForObsidianVaultPathAsync(
command.ExistingConfiguration?.Storage.RootDirectory,
cancellationToken);
// Validate Obsidian vault
var validationResult = await selectedProvider.ValidateConfigurationAsync(
new StorageOptions { RootDirectory = rootDirectory },
cancellationToken);
if (!validationResult.IsSuccess)
{
_wizardUI.ShowError($"Invalid Obsidian vault: {validationResult.Error}");
return Result<ConfigurationSettings>.Failure(validationResult.Error);
}
// Optional: Ask for subdirectory
var useSubdirectory = await _wizardUI.PromptYesNoAsync(
"Would you like to store TST entries in a subdirectory (e.g., 'ten-second-tom/')?",
defaultYes: true,
cancellationToken);
string? subdirectory = null;
if (useSubdirectory)
{
subdirectory = await _wizardUI.PromptForSubdirectoryAsync(
defaultValue: "ten-second-tom",
cancellationToken);
}
}
else // default provider
{
rootDirectory = await _wizardUI.PromptForMemoryDirectoryAsync(
command.ExistingConfiguration?.Storage.RootDirectory,
cancellationToken);
}
ISetupWizardUI Extensions
/// <summary>
/// Prompts user to select a storage provider.
/// </summary>
Task<IStorageProvider> PromptForStorageProviderAsync(
IReadOnlyList<IStorageProvider> availableProviders,
string? currentProviderId,
CancellationToken cancellationToken);
/// <summary>
/// Prompts user for Obsidian vault path with validation.
/// </summary>
Task<string> PromptForObsidianVaultPathAsync(
string? currentPath,
CancellationToken cancellationToken);
/// <summary>
/// Prompts user for a subdirectory within the storage root.
/// </summary>
Task<string?> PromptForSubdirectoryAsync(
string defaultValue,
CancellationToken cancellationToken);
5. Dependency Injection Registration
// In ServiceCollectionExtensions.cs
public static IServiceCollection AddStorageServices(this IServiceCollection services, IConfiguration configuration)
{
// Register Options Pattern
services.Configure<StorageOptions>(configuration.GetSection(StorageOptions.SectionName));
services.AddSingleton<IValidateOptions<StorageOptions>, StorageOptionsValidator>();
// Register storage provider factory
services.AddSingleton<StorageProviderFactory>();
// Register active storage provider (resolved at runtime based on configuration)
services.AddSingleton<IStorageProvider>(sp =>
{
var options = sp.GetRequiredService<IOptions<StorageOptions>>().Value;
var factory = sp.GetRequiredService<StorageProviderFactory>();
var provider = factory.CreateProvider(options.ProviderId);
// Initialize provider synchronously (or make this async with a hosted service)
provider.InitializeAsync(options, CancellationToken.None)
.GetAwaiter()
.GetResult();
return provider;
});
// Backward compatibility: IMemoryStorageProvider resolves to active IStorageProvider
services.AddSingleton<IMemoryStorageProvider>(sp => sp.GetRequiredService<IStorageProvider>());
return services;
}
Proposed Constitution Amendment (For Future Consideration)
Note: This section documents the proposed constitutional changes that SHOULD be considered when this feature is approved and implemented. The constitution has NOT been amended yet - this is a recommendation for alignment.
Proposed New Principle: IX. Extensible Storage Architecture
All storage implementations MUST use the provider-based architecture with automatic discovery.
- Storage providers MUST implement
IStorageProvider interface (which extends IMemoryStorageProvider)
- Providers MUST be automatically discovered via assembly scanning at startup
- Each provider MUST have a unique
ProviderId string for configuration selection
- Each provider MUST have
DisplayName and Description for user selection during setup
- Providers MUST validate their configuration before initialization via
ValidateConfigurationAsync
- Providers MUST implement
InitializeAsync to set up storage-specific structures
- Default provider MUST maintain backward compatibility with existing TST installations
- Setup wizard MUST allow users to select from all discovered providers
- Configuration MUST use Options Pattern with
StorageOptions for provider settings
- Storage providers belong in
Infrastructure/Storage/Providers/ directory
- Provider registration MUST use factory pattern with
StorageProviderFactory
- Cross-provider data migration tools SHOULD be provided for user convenience
Storage Provider Registration Example:
// Providers are discovered automatically via assembly scanning
public sealed class StorageProviderFactory
{
private Dictionary<string, Type> DiscoverProviders()
{
var providers = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
var assembly = typeof(ServiceCollectionExtensions).Assembly;
var providerTypes = assembly.GetTypes()
.Where(t => typeof(IStorageProvider).IsAssignableFrom(t)
&& !t.IsInterface && !t.IsAbstract)
.ToList();
foreach (var type in providerTypes)
{
var instance = (IStorageProvider)Activator.CreateInstance(type)!;
providers[instance.ProviderId] = type;
}
return providers;
}
}
// DI registration resolves active provider at runtime
services.AddSingleton<IStorageProvider>(sp =>
{
var options = sp.GetRequiredService<IOptions<StorageOptions>>().Value;
var factory = sp.GetRequiredService<StorageProviderFactory>();
var provider = factory.CreateProvider(options.ProviderId);
// Initialize provider
provider.InitializeAsync(options, CancellationToken.None)
.GetAwaiter().GetResult();
return provider;
});
Storage Provider Implementation Example:
namespace TenSecondTom.Infrastructure.Storage.Providers;
/// <summary>
/// Obsidian vault storage provider.
/// Stores memory entries in an Obsidian vault with optional subdirectory organization.
/// </summary>
public sealed class ObsidianStorageProvider : IStorageProvider
{
public string ProviderId => "obsidian";
public string DisplayName => "Obsidian Vault";
public string Description => "Store entries in an Obsidian vault for seamless note integration.";
public async Task<Result> InitializeAsync(StorageOptions options, CancellationToken cancellationToken)
{
// Validate Obsidian vault structure
string vaultRoot = options.RootDirectory;
string obsidianDir = Path.Combine(vaultRoot, ".obsidian");
if (!Directory.Exists(obsidianDir))
{
return Result.Failure($"Not a valid Obsidian vault: .obsidian directory not found");
}
// Create TST subdirectory if specified
string memoryDir = string.IsNullOrEmpty(options.MemorySubdirectory)
? vaultRoot
: Path.Combine(vaultRoot, options.MemorySubdirectory);
if (!Directory.Exists(memoryDir))
{
Directory.CreateDirectory(memoryDir);
}
return Result.Success();
}
public async Task<Result> ValidateConfigurationAsync(StorageOptions options)
{
// Validate vault structure and write permissions
// Return Result.Success() or Result.Failure(errorMessage)
}
// Implement IMemoryStorageProvider interface methods
// (SaveAsync, GetEntriesAsync, SearchEntriesAsync, etc.)
}
Rationale: The provider pattern enables extensibility without modifying core application code. Automatic discovery via assembly scanning makes adding new providers a "drop-in" experience, consistent with MediatR handler and FluentValidation validator discovery patterns established in Principle IV. This architecture supports integration with third-party tools (Obsidian, Notion, Joplin, LogSeq) while maintaining TST's native storage as the default. The factory pattern ensures proper initialization and validation before use. The Options Pattern provides type-safe configuration for provider selection and settings.
Proposed Project Structure Updates
Updated Infrastructure/Storage Directory:
src/Infrastructure/Storage/
├── Providers/ # [NEW] Storage provider implementations (auto-discovered)
│ ├── DefaultStorageProvider.cs
│ └── ObsidianStorageProvider.cs
├── IStorageProvider.cs # [NEW] Provider abstraction interface
├── IMemoryStorageProvider.cs # [EXISTING] Memory operations interface
├── StorageProviderFactory.cs # [NEW] Assembly scanning factory
├── FileSystemStorageProvider.cs # [EXISTING] Internal to DefaultStorageProvider
├── MarkdownFormatter.cs
└── AutoPurgeService.cs
tests/TenSecondTom.Tests/Infrastructure/Storage/
└── Providers/ # [NEW] Storage provider unit tests
├── DefaultStorageProviderTests.cs
└── ObsidianStorageProviderTests.cs
tests/TenSecondTom.IntegrationTests/Infrastructure/Storage/
└── Providers/ # [NEW] Storage provider integration tests
├── DefaultProviderIntegrationTests.cs
└── ObsidianProviderIntegrationTests.cs
Proposed Version Bump
Current Constitution Version: 1.4.0 (from git)
Proposed Version: 1.8.0 (MINOR bump)
Justification: Adding a new principle (Principle IX) is a MINOR version change per the constitution's governance rules. This does not break existing principles but adds new architectural guidance for storage extensibility.
Implementation Phases
Phase 0: Architecture Refactoring (Foundation)
Tasks:
- Create
IStorageProvider interface extending IMemoryStorageProvider
- Create
StorageProviderFactory with assembly scanning
- Refactor
StorageOptions (add ProviderId, RootDirectory, MemorySubdirectory)
- Create
StorageOptionsValidator updates
- Update DI registration to use factory pattern
- TEST: Ensure existing installations continue to work (backward compatibility)
No user-visible changes yet - internal refactoring only.
Phase 1: Default Provider Implementation
Tasks:
- Create
DefaultStorageProvider wrapping existing FileSystemStorageProvider
- Update setup wizard to default to "default" provider (no UI changes)
- Migrate configuration:
MemoryDirectory → Storage.RootDirectory
- TEST: New installations work with default provider
- TEST: Migration from old config format works
Phase 2: Obsidian Provider Implementation
Tasks:
- Create
ObsidianStorageProvider with vault validation
- Implement Obsidian-friendly file naming and organization
- Add setup wizard UI for provider selection
- Add Obsidian vault path selection with validation
- Add subdirectory prompt for Obsidian vaults
- TEST: Obsidian vault integration end-to-end
- TEST: Switch between providers works correctly
Phase 3: Documentation & Polish
Tasks:
- Update README with storage provider documentation
- Add Obsidian integration guide
- Add migration guide for existing users
- Update constitution (version 1.8.0)
- Update CLAUDE.md with storage provider guidance
- Add integration tests for both providers
Migration Strategy for Existing Users
Automatic Migration
// In ApplicationBootstrapper or migration service
public async Task MigrateStorageConfigurationAsync()
{
var legacyMemoryDir = _configuration["TenSecondTom:MemoryDirectory"];
if (!string.IsNullOrEmpty(legacyMemoryDir))
{
// Migrate to new format
var newConfig = new
{
TenSecondTom = new
{
Storage = new
{
ProviderId = "default",
RootDirectory = legacyMemoryDir,
MemorySubdirectory = (string?)null
}
}
};
// Save to user secrets or config file
await _configStorageService.MigrateAsync(newConfig);
_logger.LogInformation("Migrated legacy MemoryDirectory configuration to Storage.RootDirectory");
}
}
User Communication
On first run after upgrade:
Ten Second Tom has been upgraded to support multiple storage providers!
Your existing data location (/Users/chris/ten-second-tom) has been preserved.
You can now optionally switch to Obsidian vault storage:
tom config --set storage.providerid obsidian
tom setup # Re-run setup to configure Obsidian vault
Learn more: https://github.com/sirkirby/ten-second-tom/wiki/Storage-Providers
Testing Requirements
Unit Tests
StorageProviderFactoryTests: Assembly scanning, provider discovery
DefaultStorageProviderTests: Backward compatibility, initialization
ObsidianStorageProviderTests: Vault validation, file structure
StorageOptionsValidatorTests: Configuration validation
Integration Tests
DefaultProviderIntegrationTests: End-to-end with default provider
ObsidianProviderIntegrationTests: End-to-end with Obsidian vault
ProviderSwitchingTests: Switch between providers without data loss
MigrationTests: Legacy config migration
Test Coverage Goal
85% minimum (exceeds constitutional 80% requirement due to architectural significance)
Future Extensibility
Potential Future Providers
- NotionStorageProvider: Sync entries to Notion databases
- JoplinStorageProvider: Store in Joplin note application
- LogSeqStorageProvider: LogSeq outliner integration
- RemoteStorageProvider: Sync to remote Git repository
- DatabaseStorageProvider: PostgreSQL/SQLite for advanced querying
- CloudStorageProvider: S3/Azure Blob/Google Cloud Storage
Provider Discovery Convention
// Providers are discovered by:
// 1. Implementing IStorageProvider
// 2. Being in the main assembly (src/TenSecondTom.csproj)
// 3. Having a public parameterless constructor for factory discovery
// Future: Could support external provider assemblies via plugin system
Risks & Mitigation
| Risk |
Impact |
Mitigation |
| Breaking existing installations |
HIGH |
Phase 0 includes backward compatibility layer; automatic migration |
| Obsidian vault corruption |
HIGH |
Read-only validation mode; extensive integration tests; user confirmation prompts |
| Configuration migration failure |
MEDIUM |
Graceful fallback to legacy config; clear error messages |
| Performance degradation |
LOW |
Providers use same underlying FileSystemStorageProvider implementation |
| User confusion during setup |
MEDIUM |
Clear UI prompts; default to existing behavior; comprehensive documentation |
Success Criteria
- ✅ Existing TST installations continue working without user action
- ✅ New users can select storage provider during initial setup
- ✅ Obsidian users can store TST entries in their vaults
- ✅ Files created in Obsidian appear in TST and vice versa (bidirectional)
- ✅ Configuration follows Options Pattern (no magic strings)
- ✅ Test coverage ≥ 85%
- ✅ Constitution updated to version 1.8.0 with new principle
- ✅ Documentation complete and comprehensive
Obsidian Storage Provider - Research & Proposed Architecture Plan
Establish a specification to build support for Obsidian and other storage providers, including migration of existing default storage into this new model.
Executive Summary
This document outlines the research, architectural design, and proposed constitutional amendments for adding pluggable storage provider support to Ten Second Tom, with Obsidian vault integration as the first alternative provider.
Important: This is a planning document. The constitution has NOT been amended yet. The proposed Principle IX is documented here for review and will be formally added to the constitution if this feature is approved for implementation.
Key Objectives
IStorageProviderabstraction with automatic discovery via assembly scanningDefaultStorageProvider(current behavior, TST-native structure)ObsidianStorageProvider(Obsidian vault-compatible structure)Research: Obsidian Vault Structure
Obsidian Vault Characteristics (from user screenshot)
Key Insights:
TST Current Structure (from codebase analysis)
Current Issues:
Architectural Design
1. Storage Provider Abstraction
IStorageProviderInterface (extendsIMemoryStorageProvider)Storage Provider Discovery (Assembly Scanning)
2. Configuration Refactoring
Current Problem
Proposed Solution
3. Storage Provider Implementations
DefaultStorageProvider (TST-native)
ObsidianStorageProvider
4. Setup Wizard Integration
Updated SetupCommandHandler Flow
ISetupWizardUI Extensions
5. Dependency Injection Registration
Proposed Constitution Amendment (For Future Consideration)
Note: This section documents the proposed constitutional changes that SHOULD be considered when this feature is approved and implemented. The constitution has NOT been amended yet - this is a recommendation for alignment.
Proposed New Principle: IX. Extensible Storage Architecture
All storage implementations MUST use the provider-based architecture with automatic discovery.
IStorageProviderinterface (which extendsIMemoryStorageProvider)ProviderIdstring for configuration selectionDisplayNameandDescriptionfor user selection during setupValidateConfigurationAsyncInitializeAsyncto set up storage-specific structuresStorageOptionsfor provider settingsInfrastructure/Storage/Providers/directoryStorageProviderFactoryStorage Provider Registration Example:
Storage Provider Implementation Example:
Rationale: The provider pattern enables extensibility without modifying core application code. Automatic discovery via assembly scanning makes adding new providers a "drop-in" experience, consistent with MediatR handler and FluentValidation validator discovery patterns established in Principle IV. This architecture supports integration with third-party tools (Obsidian, Notion, Joplin, LogSeq) while maintaining TST's native storage as the default. The factory pattern ensures proper initialization and validation before use. The Options Pattern provides type-safe configuration for provider selection and settings.
Proposed Project Structure Updates
Updated Infrastructure/Storage Directory:
Proposed Version Bump
Current Constitution Version: 1.4.0 (from git)
Proposed Version: 1.8.0 (MINOR bump)
Justification: Adding a new principle (Principle IX) is a MINOR version change per the constitution's governance rules. This does not break existing principles but adds new architectural guidance for storage extensibility.
Implementation Phases
Phase 0: Architecture Refactoring (Foundation)
Tasks:
IStorageProviderinterface extendingIMemoryStorageProviderStorageProviderFactorywith assembly scanningStorageOptions(addProviderId,RootDirectory,MemorySubdirectory)StorageOptionsValidatorupdatesNo user-visible changes yet - internal refactoring only.
Phase 1: Default Provider Implementation
Tasks:
DefaultStorageProviderwrapping existingFileSystemStorageProviderMemoryDirectory→Storage.RootDirectoryPhase 2: Obsidian Provider Implementation
Tasks:
ObsidianStorageProviderwith vault validationPhase 3: Documentation & Polish
Tasks:
Migration Strategy for Existing Users
Automatic Migration
User Communication
On first run after upgrade:
Testing Requirements
Unit Tests
StorageProviderFactoryTests: Assembly scanning, provider discoveryDefaultStorageProviderTests: Backward compatibility, initializationObsidianStorageProviderTests: Vault validation, file structureStorageOptionsValidatorTests: Configuration validationIntegration Tests
DefaultProviderIntegrationTests: End-to-end with default providerObsidianProviderIntegrationTests: End-to-end with Obsidian vaultProviderSwitchingTests: Switch between providers without data lossMigrationTests: Legacy config migrationTest Coverage Goal
85% minimum (exceeds constitutional 80% requirement due to architectural significance)
Future Extensibility
Potential Future Providers
Provider Discovery Convention
Risks & Mitigation
Success Criteria