using System.Text.Json; using Dapper; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using PlotLine.Data; using PlotLine.Services; var key = "ConnectionStrings__DefaultConnection="; var connectionString = File.ReadLines("/etc/plotdirector/plotdirector.env").First(x => x.StartsWith(key, StringComparison.Ordinal))[key.Length..]; var factory = new SqlConnectionFactory(new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { { "ConnectionStrings:DefaultConnection", connectionString } }).Build()); using var connection = factory.CreateConnection(); connection.Open(); if (connection.Database != "PlotDirector_Development") throw new Exception("Unexpected database"); var targets = (await connection.QueryAsync<(int GenerationID, int ProjectID, int BookID, int UserID)>(""" SELECT g.ReviewCandidateGenerationID GenerationID,g.ProjectID,g.BookID, (SELECT TOP(1) UserID FROM dbo.ProjectUserAccess WHERE ProjectID=g.ProjectID AND IsActive=1 ORDER BY CASE WHEN AccessRole=N'Owner' THEN 0 ELSE 1 END,UserID) UserID FROM dbo.StoryIntelligenceReviewCandidateGenerations g WHERE g.IsCurrent=1 AND g.Status=N'Ready' AND g.Module=N'Relationships' AND EXISTS (SELECT 1 FROM dbo.StoryIntelligenceReviewCandidates c WHERE c.ReviewCandidateGenerationID=g.ReviewCandidateGenerationID AND c.DecisionStatus=N'Pending') """)).ToList(); var pipelines = new StoryIntelligencePipelineStateService(new StoryIntelligencePipelineRepository(factory), NullLogger.Instance); var builder = new StoryIntelligenceRelationshipImportService(new StoryIntelligenceResultRepository(factory), new CharacterRepository(factory), new SceneRepository(factory), pipelines, NullLogger.Instance); var repository = new StoryIntelligenceReviewCandidateRepository(factory); foreach (var target in targets) { var runs = (await pipelines.ListCommittedRunsByBookAsync(target.BookID, target.UserID)) .GroupBy(x => x.ChapterID).Select(g => g.OrderByDescending(x => x.StoryIntelligenceRunID).First()).ToList(); Console.WriteLine($"Committed runs={runs.Count}; Active characters={(await new CharacterRepository(factory).ListCharactersAsync(target.ProjectID)).Count}"); var runRows = await new StoryIntelligenceResultRepository(factory).ListRunsByBookForUserAsync(target.BookID, target.UserID); Console.WriteLine($"Analysis runs={runRows.Count}"); var batch = new OnboardingStoryIntelligenceBatch { ProjectID = target.ProjectID, BookID = target.BookID, UserID = target.UserID, Items = runs.Count > 0 ? runs.OrderByDescending(x => x.ChapterNumber).Select(x => new OnboardingStoryIntelligenceBatchItem { ChapterID = x.ChapterID, ChapterNumber = (int)x.ChapterNumber, RunID = x.StoryIntelligenceRunID }).ToList() : runRows.Where(x => x.ChapterID.HasValue).OrderByDescending(x => x.ChapterNumber).Select(x => new OnboardingStoryIntelligenceBatchItem { ChapterID = x.ChapterID!.Value, ChapterNumber = (int)(x.ChapterNumber ?? 0), RunID = x.StoryIntelligenceRunID }).ToList() }; // Deliberately reverse input chapters to verify first/last labels are independent of processing order. var review = await builder.BuildReviewAsync(batch); var sorted = review.Candidates.OrderBy(x => x.FirstChapterNumber).ThenBy(x => x.FirstSceneNumber).ThenBy(x => x.FirstSceneID).Select(x => x.Key); if (!review.Candidates.Select(x => x.Key).SequenceEqual(sorted)) throw new Exception("Nonchronological candidates"); Console.WriteLine($"Book={target.BookID}; Candidates={review.Candidates.Count}; Types={string.Join(", ", review.Candidates.GroupBy(x => x.RelationshipType).Select(g => $"{g.Key}:{g.Count()}"))}"); var json = JsonSerializer.Serialize(review.Candidates, new JsonSerializerOptions(JsonSerializerDefaults.Web)); if (args.Contains("--apply")) { var previous = new List(); for (var pageNumber = 1; ; pageNumber++) { var page = await repository.ListPageAsync(target.BookID, "Relationships", pageNumber, 100); previous.AddRange(page); if (page.Count < 100) break; } var backup = $"/var/tmp/plotdirector-relationship-review-{target.BookID}-{DateTime.UtcNow:yyyyMMddHHmmss}.json"; await File.WriteAllTextAsync(backup, JsonSerializer.Serialize(previous)); Console.WriteLine($"Pending snapshot: {backup}"); await repository.RefreshPendingRelationshipsAsync(target.BookID, target.GenerationID, json); Console.WriteLine($"Refreshed pending suggestions for book {target.BookID}; author decisions preserved."); } }