From 6a2aeeea90b1975e0f7adb7eaea2e1988fb6bd46 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Mon, 15 Jun 2026 09:27:31 +0100 Subject: [PATCH] Phase 12I: Knowledge Regression Warnings. --- PlotLine/Services/CoreServices.cs | 134 +++++++++++++++++++------- PlotLine/ViewModels/CoreViewModels.cs | 14 +++ PlotLine/Views/Warnings/Index.cshtml | 18 ++++ 3 files changed, 130 insertions(+), 36 deletions(-) diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index c6dab5b..64da3d8 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -2544,7 +2544,7 @@ public sealed class StoryStateService( warnings.AddRange(BuildMultipleCharacterLocationWarnings(characterTimeline.Appearances, enabledCharacterIds, context)); warnings.AddRange(BuildImpossibleMovementWarnings(characterTimeline.Appearances, enabledCharacterIds, context)); warnings.AddRange(BuildUnknownCharacterLocationWarnings(characterTimeline.Characters, characterTimeline.Appearances, enabledCharacterIds, context)); - warnings.AddRange(await BuildKnowledgeChronologyWarningsAsync(characterTimeline.Characters, enabledCharacterIds, context)); + warnings.AddRange(await BuildKnowledgeRegressionWarningsAsync(characterTimeline.Characters, enabledCharacterIds, context)); } if (filter.IncludeAssets) @@ -2975,7 +2975,7 @@ public sealed class StoryStateService( .ToList(); } - private async Task> BuildKnowledgeChronologyWarningsAsync( + private async Task> BuildKnowledgeRegressionWarningsAsync( IReadOnlyList projectCharacters, IReadOnlySet enabledCharacterIds, RangeContext context) @@ -2996,54 +2996,52 @@ public sealed class StoryStateService( foreach (var group in knowledgeEvents.GroupBy(KnowledgeIdentity)) { - var established = group - .Where(x => string.Equals(x.KnowledgeStateName, "Knows", StringComparison.OrdinalIgnoreCase)) - .OrderBy(x => context.SceneIndexes.GetValueOrDefault(x.SceneID, int.MaxValue)) - .ThenBy(x => x.CreatedDate) - .ThenBy(x => x.CharacterKnowledgeID) - .FirstOrDefault(); - - if (established is null) - { - continue; - } - - var establishedSceneIndex = context.SceneIndexes.GetValueOrDefault(established.SceneID, int.MaxValue); - var earlierReferences = group - .Where(x => x.CharacterKnowledgeID != established.CharacterKnowledgeID - && context.AllowedSceneIds.Contains(x.SceneID) - && context.SceneIndexes.GetValueOrDefault(x.SceneID, int.MaxValue) < establishedSceneIndex) - .GroupBy(x => x.SceneID) - .Select(x => x.OrderBy(row => row.CreatedDate).ThenBy(row => row.CharacterKnowledgeID).First()) + var progressionEvents = group + .Select(x => new { Item = x, Rank = KnowledgeStateRank(x.KnowledgeStateName) }) + .Where(x => x.Rank.HasValue) + .OrderBy(x => context.SceneIndexes.GetValueOrDefault(x.Item.SceneID, int.MaxValue)) + .ThenBy(x => x.Item.CreatedDate) + .ThenBy(x => x.Item.CharacterKnowledgeID) .ToList(); - foreach (var reference in earlierReferences) + for (var i = 1; i < progressionEvents.Count; i++) { + var previous = progressionEvents[i - 1]; + var current = progressionEvents[i]; + if (current.Rank!.Value >= previous.Rank!.Value || !context.AllowedSceneIds.Contains(current.Item.SceneID)) + { + continue; + } + + var knowledgeDescription = KnowledgeTitle(current.Item); var warning = BuildWarning( - $"Knowledge Chronology Violation|K:{established.CharacterKnowledgeID}", + $"KnowledgeRegression|Knowledge:{NormaliseWarningKeyPart(knowledgeDescription)}", "Warning", - $"{character.CharacterName} appears to know \"{KnowledgeTitle(established)}\" before this knowledge is established.", - reference.SceneID, + KnowledgeRegressionMessage(character.CharacterName, knowledgeDescription, previous.Item.KnowledgeStateName, current.Item.KnowledgeStateName), + current.Item.SceneID, context, characterId: character.CharacterID); - warning.WarningTypeDisplayName = "Knowledge Chronology Violation"; + warning.WarningTypeDisplayName = "Knowledge Regression"; warning.CharacterName = character.CharacterName; - warning.KnowledgeId = established.CharacterKnowledgeID; - warning.KnowledgeTitle = KnowledgeTitle(established); - warning.EstablishedSceneId = established.SceneID; - warning.EstablishedSceneDisplayName = context.SceneContexts.GetValueOrDefault(established.SceneID)?.SceneDisplayName - ?? context.SceneLabels.GetValueOrDefault(established.SceneID, SceneLabel(established.SceneNumber, established.SceneTitle)); - warning.ReferencedSceneId = reference.SceneID; - warning.ReferencedSceneDisplayName = context.SceneContexts.GetValueOrDefault(reference.SceneID)?.SceneDisplayName - ?? context.SceneLabels.GetValueOrDefault(reference.SceneID, SceneLabel(reference.SceneNumber, reference.SceneTitle)); + warning.KnowledgeId = current.Item.CharacterKnowledgeID; + warning.KnowledgeTitle = knowledgeDescription; + warning.KnowledgeDescription = knowledgeDescription; + warning.PreviousKnowledgeState = previous.Item.KnowledgeStateName; + warning.CurrentKnowledgeState = current.Item.KnowledgeStateName; + warning.PreviousSceneId = previous.Item.SceneID; + warning.PreviousSceneDisplayName = context.SceneContexts.GetValueOrDefault(previous.Item.SceneID)?.SceneDisplayName + ?? context.SceneLabels.GetValueOrDefault(previous.Item.SceneID, SceneLabel(previous.Item.SceneNumber, previous.Item.SceneTitle)); + warning.CurrentSceneId = current.Item.SceneID; + warning.CurrentSceneDisplayName = context.SceneContexts.GetValueOrDefault(current.Item.SceneID)?.SceneDisplayName + ?? context.SceneLabels.GetValueOrDefault(current.Item.SceneID, SceneLabel(current.Item.SceneNumber, current.Item.SceneTitle)); warnings.Add(warning); } } } return warnings - .GroupBy(x => new { x.CharacterID, x.KnowledgeId, x.ReferencedSceneId, x.EstablishedSceneId }) + .GroupBy(x => new { x.CharacterID, x.KnowledgeDescription, x.CurrentSceneId }) .Select(x => x.First()) .ToList(); } @@ -3257,6 +3255,63 @@ public sealed class StoryStateService( return string.IsNullOrWhiteSpace(item.Description) ? "Knowledge" : item.Description.Trim(); } + private static int? KnowledgeStateRank(string stateName) + { + return stateName switch + { + "Unaware" => 0, + "Suspects" => 1, + "Partially Knows" => 2, + "Knows" => 3, + _ => null + }; + } + + private static string KnowledgeRegressionMessage(string characterName, string knowledgeDescription, string previousState, string currentState) + { + if (string.Equals(previousState, "Knows", StringComparison.OrdinalIgnoreCase) + && string.Equals(currentState, "Unaware", StringComparison.OrdinalIgnoreCase)) + { + return $"{characterName} appears unaware of \"{knowledgeDescription}\" after previously knowing it."; + } + + if (string.Equals(previousState, "Knows", StringComparison.OrdinalIgnoreCase) + && string.Equals(currentState, "Suspects", StringComparison.OrdinalIgnoreCase)) + { + return $"{characterName} appears to regress from knowing to suspecting \"{knowledgeDescription}\"."; + } + + if (string.Equals(previousState, "Knows", StringComparison.OrdinalIgnoreCase) + && string.Equals(currentState, "Partially Knows", StringComparison.OrdinalIgnoreCase)) + { + return $"{characterName} appears to lose understanding of \"{knowledgeDescription}\"."; + } + + if (string.Equals(currentState, "Unaware", StringComparison.OrdinalIgnoreCase)) + { + return $"{characterName} appears to lose awareness of \"{knowledgeDescription}\"."; + } + + return $"{characterName} appears to regress from {KnowledgeStatePhrase(previousState)} to {KnowledgeStatePhrase(currentState)} \"{knowledgeDescription}\"."; + } + + private static string KnowledgeStatePhrase(string stateName) + { + return stateName switch + { + "Unaware" => "being unaware of", + "Suspects" => "suspecting", + "Partially Knows" => "partially knowing", + "Knows" => "knowing", + _ => stateName.ToLowerInvariant() + }; + } + + private static string NormaliseWarningKeyPart(string value) + { + return string.Join(" ", value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + } + private static void ApplyAcknowledgements( IReadOnlyList warnings, IReadOnlyList acknowledgements) @@ -4080,6 +4135,13 @@ public sealed class ContinuityValidationService( WarningKey = warning.WarningKey, KnowledgeId = warning.KnowledgeId, KnowledgeTitle = warning.KnowledgeTitle, + KnowledgeDescription = warning.KnowledgeDescription, + PreviousKnowledgeState = warning.PreviousKnowledgeState, + CurrentKnowledgeState = warning.CurrentKnowledgeState, + PreviousSceneId = warning.PreviousSceneId, + PreviousSceneDisplayName = warning.PreviousSceneDisplayName, + CurrentSceneId = warning.CurrentSceneId, + CurrentSceneDisplayName = warning.CurrentSceneDisplayName, EstablishedSceneId = warning.EstablishedSceneId, EstablishedSceneDisplayName = warning.EstablishedSceneDisplayName, ReferencedSceneId = warning.ReferencedSceneId, @@ -4103,7 +4165,7 @@ public sealed class ContinuityValidationService( private static string WarningCategoryForGenerated(ContinuityWarningViewModel warning) { - if (string.Equals(warning.WarningTypeDisplayName, "Knowledge Chronology Violation", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(warning.WarningTypeDisplayName, "Knowledge Regression", StringComparison.OrdinalIgnoreCase)) { return "Knowledge"; } diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index 6a11a76..09e4400 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -333,6 +333,13 @@ public sealed class ContinuityWarningViewModel public string? AcknowledgementNotes { get; set; } public int? KnowledgeId { get; set; } public string KnowledgeTitle { get; set; } = string.Empty; + public string KnowledgeDescription { get; set; } = string.Empty; + public string PreviousKnowledgeState { get; set; } = string.Empty; + public string CurrentKnowledgeState { get; set; } = string.Empty; + public int? PreviousSceneId { get; set; } + public string PreviousSceneDisplayName { get; set; } = string.Empty; + public int? CurrentSceneId { get; set; } + public string CurrentSceneDisplayName { get; set; } = string.Empty; public int? EstablishedSceneId { get; set; } public string EstablishedSceneDisplayName { get; set; } = string.Empty; public int? ReferencedSceneId { get; set; } @@ -1689,6 +1696,13 @@ public sealed class ProjectWarningViewModel public string WarningKey { get; set; } = string.Empty; public int? KnowledgeId { get; set; } public string KnowledgeTitle { get; set; } = string.Empty; + public string KnowledgeDescription { get; set; } = string.Empty; + public string PreviousKnowledgeState { get; set; } = string.Empty; + public string CurrentKnowledgeState { get; set; } = string.Empty; + public int? PreviousSceneId { get; set; } + public string PreviousSceneDisplayName { get; set; } = string.Empty; + public int? CurrentSceneId { get; set; } + public string CurrentSceneDisplayName { get; set; } = string.Empty; public int? EstablishedSceneId { get; set; } public string EstablishedSceneDisplayName { get; set; } = string.Empty; public int? ReferencedSceneId { get; set; } diff --git a/PlotLine/Views/Warnings/Index.cshtml b/PlotLine/Views/Warnings/Index.cshtml index a9211e7..f83dd5f 100644 --- a/PlotLine/Views/Warnings/Index.cshtml +++ b/PlotLine/Views/Warnings/Index.cshtml @@ -175,6 +175,24 @@ { Knowledge: @warning.KnowledgeTitle } + @if (!string.IsNullOrWhiteSpace(warning.PreviousKnowledgeState) && !string.IsNullOrWhiteSpace(warning.CurrentKnowledgeState)) + { + State: @warning.PreviousKnowledgeState → @warning.CurrentKnowledgeState + } + @if (warning.PreviousSceneId.HasValue) + { + + Previous: + @warning.PreviousSceneDisplayName + + } + @if (warning.CurrentSceneId.HasValue) + { + + Current: + @warning.CurrentSceneDisplayName + + } @if (warning.EstablishedSceneId.HasValue) {