using PlotLine.Data; using PlotLine.Models; namespace PlotLine.Services; public interface IStoryIntelligencePipelineStateService { Task GetForBookAsync(int bookId, int userId); Task GetReviewCentreSummaryAsync(int bookId, int userId); Task> ListForUserAsync(int userId); Task EnsureForBookAsync(int bookId, int userId); Task RecordChapterAnalysisQueuedAsync(int projectId, int bookId, int runId); Task RecordSceneImportAsync(int projectId, int bookId, int runId); Task RecordCharacterImportAsync(int projectId, int bookId); Task RecordLocationImportAsync(int projectId, int bookId); Task RecordAssetImportAsync(int projectId, int bookId); Task RecordRelationshipImportAsync(int projectId, int bookId); Task RecordKnowledgeImportAsync(int projectId, int bookId); Task RecordNarrativeImportAsync(int projectId, int bookId); Task CompleteSceneAnalysisIfReadyAsync(int bookId, int userId); Task FindNextReadySceneAnalysisAsync(); Task> ListCommittedRunsByBookAsync(int bookId, int userId); } public sealed class StoryIntelligencePipelineStateService( IStoryIntelligencePipelineRepository pipelines, ILogger logger) : IStoryIntelligencePipelineStateService { public async Task GetForBookAsync(int bookId, int userId) => await pipelines.GetByBookForUserAsync(bookId, userId) ?? await EnsureForBookAsync(bookId, userId); public Task GetReviewCentreSummaryAsync(int bookId, int userId) => pipelines.GetReviewCentreSummaryAsync(bookId, userId); public Task> ListForUserAsync(int userId) => pipelines.ListForUserAsync(userId); public async Task EnsureForBookAsync(int bookId, int userId) { var existing = await pipelines.GetByBookForUserAsync(bookId, userId); if (existing is not null) { return existing; } var committedRuns = await pipelines.ListCommittedRunsByBookAsync(bookId, userId); if (committedRuns.Count == 0) { return null; } var latestRun = committedRuns.OrderByDescending(run => run.StoryIntelligenceRunID).First(); var created = await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = latestRun.ProjectID, BookID = latestRun.BookID, CurrentStage = StoryIntelligencePipelineStages.CharacterReview, LastCompletedStage = StoryIntelligencePipelineStages.SceneImport, CurrentReviewStage = StoryIntelligencePipelineStages.CharacterReview, Status = StoryIntelligencePipelineStatuses.NeedsReview, CompletedUtc = null, LastRunID = latestRun.StoryIntelligenceRunID }); logger.LogInformation( "Created Story Intelligence pipeline state for existing imported book {BookID}. ProjectID={ProjectID} CurrentStage={CurrentStage}", bookId, created.ProjectID, created.CurrentStage); return created; } public async Task RecordSceneImportAsync(int projectId, int bookId, int runId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.CharacterReview, LastCompletedStage = StoryIntelligencePipelineStages.SceneImport, CurrentReviewStage = StoryIntelligencePipelineStages.CharacterReview, Status = StoryIntelligencePipelineStatuses.NeedsReview, CompletedUtc = null, LastRunID = runId }); } public async Task RecordChapterAnalysisQueuedAsync(int projectId, int bookId, int runId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.Chapters, LastCompletedStage = null, CurrentReviewStage = null, Status = StoryIntelligencePipelineStatuses.InProgress, CompletedUtc = null, LastRunID = runId }); } public async Task RecordCharacterImportAsync(int projectId, int bookId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.LocationReview, LastCompletedStage = StoryIntelligencePipelineStages.CharacterImport, CurrentReviewStage = StoryIntelligencePipelineStages.LocationReview, Status = StoryIntelligencePipelineStatuses.NeedsReview, CompletedUtc = null, LastRunID = null, MarkCharacterReviewComplete = true }); } public async Task RecordLocationImportAsync(int projectId, int bookId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.AssetReview, LastCompletedStage = StoryIntelligencePipelineStages.LocationImport, CurrentReviewStage = StoryIntelligencePipelineStages.AssetReview, Status = StoryIntelligencePipelineStatuses.NeedsReview, CompletedUtc = null, LastRunID = null, MarkLocationReviewComplete = true }); } public async Task RecordAssetImportAsync(int projectId, int bookId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.RelationshipReview, LastCompletedStage = StoryIntelligencePipelineStages.AssetImport, CurrentReviewStage = StoryIntelligencePipelineStages.RelationshipReview, Status = StoryIntelligencePipelineStatuses.NeedsReview, CompletedUtc = null, LastRunID = null, MarkAssetReviewComplete = true }); } public async Task RecordRelationshipImportAsync(int projectId, int bookId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.KnowledgeReview, LastCompletedStage = StoryIntelligencePipelineStages.RelationshipImport, CurrentReviewStage = StoryIntelligencePipelineStages.KnowledgeReview, Status = StoryIntelligencePipelineStatuses.NeedsReview, CompletedUtc = null, LastRunID = null, MarkRelationshipReviewComplete = true }); } public async Task RecordKnowledgeImportAsync(int projectId, int bookId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.NarrativeReview, LastCompletedStage = StoryIntelligencePipelineStages.KnowledgeImport, CurrentReviewStage = StoryIntelligencePipelineStages.NarrativeReview, Status = StoryIntelligencePipelineStatuses.NeedsReview, CompletedUtc = null, LastRunID = null, MarkKnowledgeReviewComplete = true }); } public async Task RecordNarrativeImportAsync(int projectId, int bookId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.Complete, LastCompletedStage = StoryIntelligencePipelineStages.NarrativeImport, CurrentReviewStage = null, Status = StoryIntelligencePipelineStatuses.Complete, CompletedUtc = DateTime.UtcNow, LastRunID = null, MarkNarrativeReviewComplete = true }); } public async Task CompleteSceneAnalysisIfReadyAsync(int bookId, int userId) { var completion = await pipelines.TryCompleteSceneAnalysisAsync(bookId, userId); LogSceneAnalysisCompletion(completion, "completed"); return completion; } public Task FindNextReadySceneAnalysisAsync() => pipelines.FindNextReadySceneAnalysisAsync(); public Task> ListCommittedRunsByBookAsync(int bookId, int userId) => pipelines.ListCommittedRunsByBookAsync(bookId, userId); private void LogSceneAnalysisCompletion(StoryIntelligenceSceneAnalysisCompletion? completion, string mode) { if (completion is not { CompletedNow: true }) { return; } logger.LogInformation( "Story Intelligence scene analysis {Mode} for book {BookID}. PipelineID={PipelineID} UserID={UserID} Scenes={CompletedScenes}/{TotalExpectedScenes} FailedScenes={FailedScenes} Runs={CompletedRunCount}/{RunCount}", mode, completion.BookID, completion.StoryIntelligenceBookPipelineID, completion.UserID, completion.CompletedScenes, completion.TotalExpectedScenes, completion.FailedScenes, completion.CompletedRunCount, completion.RunCount); } }