using PlotLine.Data; using PlotLine.Models; namespace PlotLine.Services; public interface IStoryIntelligencePipelineStateService { Task GetForBookAsync(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> 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> 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 }); } 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 }); } 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 }); } 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 }); } public async Task RecordKnowledgeImportAsync(int projectId, int bookId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.Complete, LastCompletedStage = StoryIntelligencePipelineStages.KnowledgeImport, CurrentReviewStage = null, Status = StoryIntelligencePipelineStatuses.Complete, CompletedUtc = DateTime.UtcNow, LastRunID = null }); } public Task> ListCommittedRunsByBookAsync(int bookId, int userId) => pipelines.ListCommittedRunsByBookAsync(bookId, userId); }