From 279c8f182c39540e2f49bd24da0b8817fba21989 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sun, 14 Jun 2026 20:22:24 +0100 Subject: [PATCH] Investigate and fix issues discovered after Phase 12E Continuity Warnings. --- PlotLine/Data/Repositories.cs | 56 +++++++++++++++++++++++++++++-- PlotLine/Services/CoreServices.cs | 48 +++----------------------- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/PlotLine/Data/Repositories.cs b/PlotLine/Data/Repositories.cs index eba2db8..594188d 100644 --- a/PlotLine/Data/Repositories.cs +++ b/PlotLine/Data/Repositories.cs @@ -2766,14 +2766,14 @@ public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : I { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetCustodyEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); - return rows.ToList(); + return await WithValidCustodianSummariesAsync(connection, rows.ToList()); } public async Task> ListCustodyByAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetCustodyEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); - return rows.ToList(); + return await WithValidCustodianSummariesAsync(connection, rows.ToList()); } public async Task> ListCustodyCharactersByProjectAsync(int projectId) @@ -2795,12 +2795,62 @@ public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : I LEFT JOIN dbo.Characters c ON c.CharacterID = acec.CharacterID LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = acec.CustodyRoleID WHERE sa.ProjectID = @ProjectID - AND sa.IsArchived = 0; + AND sa.IsArchived = 0 + AND acec.CharacterID IS NOT NULL + AND NULLIF(LTRIM(RTRIM(c.CharacterName)), N'') IS NOT NULL; """, new { ProjectID = projectId }); return rows.ToList(); } + private static async Task> WithValidCustodianSummariesAsync(IDbConnection connection, List rows) + { + if (rows.Count == 0) + { + return rows; + } + + var eventIds = rows.Select(x => x.AssetCustodyEventID).Distinct().ToList(); + var validCustodians = await connection.QueryAsync( + """ + SELECT acec.AssetCustodyEventID, + c.CharacterName, + cr.RoleName + FROM dbo.AssetCustodyEventCharacters acec + INNER JOIN dbo.Characters c ON c.CharacterID = acec.CharacterID + LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = acec.CustodyRoleID + WHERE acec.AssetCustodyEventID IN @AssetCustodyEventIDs + AND acec.CharacterID IS NOT NULL + AND NULLIF(LTRIM(RTRIM(c.CharacterName)), N'') IS NOT NULL; + """, + new { AssetCustodyEventIDs = eventIds }); + + var summaries = validCustodians + .GroupBy(x => x.AssetCustodyEventID) + .ToDictionary( + x => x.Key, + x => string.Join(", ", x + .Select(row => string.IsNullOrWhiteSpace(row.RoleName) + ? row.CharacterName + : $"{row.CharacterName} ({row.RoleName})") + .Where(label => !string.IsNullOrWhiteSpace(label)) + .Distinct(StringComparer.OrdinalIgnoreCase))); + + foreach (var row in rows) + { + row.CustodianSummary = summaries.GetValueOrDefault(row.AssetCustodyEventID); + } + + return rows.Where(x => !string.IsNullOrWhiteSpace(x.CustodianSummary)).ToList(); + } + + private sealed class AssetCustodySummaryRow + { + public int AssetCustodyEventID { get; set; } + public string CharacterName { get; set; } = string.Empty; + public string? RoleName { get; set; } + } + public async Task SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable custodianCharacterIds, int custodyRoleId) { using var connection = connectionFactory.CreateConnection(); diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index 8e7b74c..deff798 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -2804,46 +2804,10 @@ public sealed class StoryStateService( IReadOnlySet enabledCharacterIds, RangeContext context) { - var warnings = new List(); - var rowsByCharacter = appearances - .Where(x => x.LocationID.HasValue && (!enabledCharacterIds.Any() || enabledCharacterIds.Contains(x.CharacterID))) - .GroupBy(x => x.CharacterID); - - foreach (var group in rowsByCharacter) - { - var explicitLocations = group - .GroupBy(x => x.SceneID) - .Select(sceneGroup => sceneGroup.OrderByDescending(x => x.UpdatedDate).First()) - .OrderBy(x => context.SceneIndexes.GetValueOrDefault(x.SceneID, -1)) - .ToList(); - - for (var index = 1; index < explicitLocations.Count; index++) - { - var previous = explicitLocations[index - 1]; - var current = explicitLocations[index]; - if (previous.LocationID == current.LocationID || !context.AllowedSceneIds.Contains(current.SceneID)) - { - continue; - } - - var previousIndex = context.SceneIndexes.GetValueOrDefault(previous.SceneID, -1); - var currentIndex = context.SceneIndexes.GetValueOrDefault(current.SceneID, -1); - if (currentIndex - previousIndex != 1) - { - continue; - } - - warnings.Add(BuildWarning( - "Impossible Character Movement", - "Warning", - $"{current.CharacterName} moved from {previous.LocationPath ?? previous.LocationName ?? "an unknown location"} to {current.LocationPath ?? current.LocationName ?? "an unknown location"} between consecutive scenes.", - current.SceneID, - context, - characterId: current.CharacterID)); - } - } - - return warnings; + // Disabled for now: consecutive-scene location changes are often ordinary story movement. + // Re-enable when this warning can use scene dates/times, overlapping scene spans, + // location distance, or author-configurable movement rules. + return []; } private static List BuildUnknownCharacterLocationWarnings( @@ -2902,9 +2866,7 @@ public sealed class StoryStateService( foreach (var group in directLocationGroups) { var directLocationCount = group.Select(x => x.LocationID).Distinct().Count(); - custodyGroups.TryGetValue(group.Key, out var custodyRows); - var hasCustody = custodyRows?.Any() == true; - if (directLocationCount <= 1 && !hasCustody) + if (directLocationCount <= 1) { continue; }