using System.Collections.Concurrent; using System.Text; using System.Text.Json; using AIStudio.Settings; namespace AIStudio.Assistants.VisualBriefing; /// /// Defines VisualBriefingStore for the visual briefing feature. /// public sealed partial class VisualBriefingStore( VisualBriefingArtifactService artifactService, ILogger logger, VisualBriefingStorageOptions? storageOptions = null) { /// Defines the project manifest filename. private const string MANIFEST_FILE_NAME = "manifest.json"; /// Defines the last-selection filename. private const string SELECTION_FILE_NAME = "selection.json"; /// Defines the intermediate-artifact directory. private const string ARTIFACTS_DIRECTORY_NAME = "artifacts"; /// Defines the evidence-artifact directory. private const string EVIDENCE_ARTIFACTS_DIRECTORY_NAME = "evidence"; /// Defines the plan-artifact directory. private const string PLAN_ARTIFACTS_DIRECTORY_NAME = "plan"; /// Defines the content-artifact directory. private const string CONTENT_ARTIFACTS_DIRECTORY_NAME = "content"; /// Defines the presentation-artifact directory. private const string PRESENTATION_ARTIFACTS_DIRECTORY_NAME = "presentation"; /// Defines the build-history directory. private const string BUILDS_DIRECTORY_NAME = "builds"; /// Defines the immutable-version directory. private const string VERSIONS_DIRECTORY_NAME = "versions"; /// Defines the persistent-transcript directory. private const string TRANSCRIPTS_DIRECTORY_NAME = "transcripts"; /// Gets the shared persistence JSON options. private static readonly JsonSerializerOptions JSON_OPTIONS = VisualBriefingJson.Persistence; /// Stores per-project process locks. private readonly ConcurrentDictionary briefingLocks = []; /// /// Serializes store initialization. /// private readonly SemaphoreSlim initializationLock = new(1, 1); /// /// Serializes last-selection writes. /// private readonly SemaphoreSlim selectionLock = new(1, 1); /// Tracks whether initialization and reconciliation completed. private bool initialized; /// /// Defines RootDirectory for the visual briefing feature. /// private string RootDirectory => Path.Combine( storageOptions?.DataDirectory ?? SettingsManager.DataDirectory ?? throw new InvalidOperationException("The AI Studio data directory is not initialized."), "visualBriefings"); /// /// Reads a JSON file while treating malformed persisted diagnostics as unavailable. /// /// The JSON model type. /// The file path. /// The cancellation token. /// The parsed value, or . private static async Task ReadJsonAsync(string path, CancellationToken token) where T : class { if (!File.Exists(path)) return null; try { await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 65_536, true); return await JsonSerializer.DeserializeAsync(stream, JSON_OPTIONS, token); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException) { return null; } } /// /// Writes an immutable intermediate artifact without replacing an existing file. /// /// The artifact path. /// The serialized artifact. /// The cancellation token. private static async Task WriteImmutableArtifactAsync( string path, string json, CancellationToken token) { await WriteTextAtomicAsync(path, json, token, overwrite: false); } /// /// Defines WriteTextAtomicAsync for the visual briefing feature. /// private static async Task WriteTextAtomicAsync( string targetPath, string content, CancellationToken token, bool overwrite = true) { Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); var temporaryPath = $"{targetPath}.tmp-{Guid.NewGuid():N}"; try { await File.WriteAllTextAsync(temporaryPath, content, new UTF8Encoding(false), token); await using (var stream = new FileStream(temporaryPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None, 4_096, true)) await stream.FlushAsync(token); File.Move(temporaryPath, targetPath, overwrite); } finally { TryDeleteFile(temporaryPath); } } /// /// Defines TryDeleteFile for the visual briefing feature. /// private static void TryDeleteFile(string path) { try { if (File.Exists(path)) File.Delete(path); } catch { // Startup and rollback cleanup are best effort. } } /// /// Defines PathComparer for the visual briefing feature. /// private static StringComparer PathComparer() => OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; /// /// Defines T for the visual briefing feature. /// private static bool IsNull(T? value) => value is null; /// /// Defines GetLock for the visual briefing feature. /// private SemaphoreSlim GetLock(Guid briefingId) => this.briefingLocks.GetOrAdd(briefingId, _ => new(1, 1)); /// /// Defines BriefingDirectory for the visual briefing feature. /// private string BriefingDirectory(Guid briefingId) => Path.Combine(this.RootDirectory, briefingId.ToString("D")); /// /// Defines ManifestPath for the visual briefing feature. /// private string ManifestPath(Guid briefingId) => Path.Combine(this.BriefingDirectory(briefingId), MANIFEST_FILE_NAME); /// /// Defines SelectionPath for the visual briefing feature. /// private string SelectionPath() => Path.Combine(this.RootDirectory, SELECTION_FILE_NAME); /// /// Defines VersionsDirectory for the visual briefing feature. /// private string VersionsDirectory(Guid briefingId) => Path.Combine(this.BriefingDirectory(briefingId), VERSIONS_DIRECTORY_NAME); /// /// Defines TranscriptsDirectory for the visual briefing feature. /// private string TranscriptsDirectory(Guid briefingId) => Path.Combine(this.BriefingDirectory(briefingId), TRANSCRIPTS_DIRECTORY_NAME); /// /// Defines ArtifactsDirectory for the visual briefing feature. /// private string ArtifactsDirectory(Guid briefingId) => Path.Combine(this.BriefingDirectory(briefingId), ARTIFACTS_DIRECTORY_NAME); /// /// Defines EvidenceArtifactsDirectory for the visual briefing feature. /// private string EvidenceArtifactsDirectory(Guid briefingId) => Path.Combine(this.ArtifactsDirectory(briefingId), EVIDENCE_ARTIFACTS_DIRECTORY_NAME); /// /// Defines PlanArtifactsDirectory for the visual briefing feature. /// private string PlanArtifactsDirectory(Guid briefingId) => Path.Combine(this.ArtifactsDirectory(briefingId), PLAN_ARTIFACTS_DIRECTORY_NAME); /// /// Defines ContentArtifactsDirectory for the visual briefing feature. /// private string ContentArtifactsDirectory(Guid briefingId) => Path.Combine(this.ArtifactsDirectory(briefingId), CONTENT_ARTIFACTS_DIRECTORY_NAME); /// /// Defines PresentationArtifactsDirectory for the visual briefing feature. /// private string PresentationArtifactsDirectory(Guid briefingId) => Path.Combine(this.ArtifactsDirectory(briefingId), PRESENTATION_ARTIFACTS_DIRECTORY_NAME); /// /// Defines BuildsDirectory for the visual briefing feature. /// private string BuildsDirectory(Guid briefingId) => Path.Combine(this.BriefingDirectory(briefingId), BUILDS_DIRECTORY_NAME); /// /// Defines EvidenceArtifactPath for the visual briefing feature. /// private string EvidenceArtifactPath(Guid briefingId, Guid artifactId) => Path.Combine(this.EvidenceArtifactsDirectory(briefingId), $"{artifactId:D}.json"); /// /// Defines PlanArtifactPath for the visual briefing feature. /// private string PlanArtifactPath(Guid briefingId, Guid artifactId) => Path.Combine(this.PlanArtifactsDirectory(briefingId), $"{artifactId:D}.json"); /// /// Defines ContentArtifactPath for the visual briefing feature. /// private string ContentArtifactPath(Guid briefingId, Guid artifactId) => Path.Combine(this.ContentArtifactsDirectory(briefingId), $"{artifactId:D}.json"); /// /// Defines PresentationArtifactPath for the visual briefing feature. /// private string PresentationArtifactPath(Guid briefingId, Guid artifactId) => Path.Combine(this.PresentationArtifactsDirectory(briefingId), $"{artifactId:D}.json"); /// /// Defines BuildPath for the visual briefing feature. /// private string BuildPath(Guid briefingId, Guid buildId) => Path.Combine(this.BuildsDirectory(briefingId), $"{buildId:D}.json"); /// /// Defines TranscriptPath for the visual briefing feature. /// private string TranscriptPath(Guid briefingId, Guid sourceId) => Path.Combine(this.TranscriptsDirectory(briefingId), $"{sourceId:D}.md"); /// /// Defines VersionPath for the visual briefing feature. /// private string VersionPath(Guid briefingId, VisualBriefingVersion version) => Path.Combine(this.VersionsDirectory(briefingId), version.FileName); }