Phase 12I: Knowledge Regression Warnings.
This commit is contained in:
parent
f401416fbd
commit
6a2aeeea90
@ -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<List<ContinuityWarningViewModel>> BuildKnowledgeChronologyWarningsAsync(
|
||||
private async Task<List<ContinuityWarningViewModel>> BuildKnowledgeRegressionWarningsAsync(
|
||||
IReadOnlyList<Character> projectCharacters,
|
||||
IReadOnlySet<int> 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<ContinuityWarningViewModel> warnings,
|
||||
IReadOnlyList<ContinuityWarningAcknowledgement> 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";
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -175,6 +175,24 @@
|
||||
{
|
||||
<small class="text-muted d-block">Knowledge: @warning.KnowledgeTitle</small>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(warning.PreviousKnowledgeState) && !string.IsNullOrWhiteSpace(warning.CurrentKnowledgeState))
|
||||
{
|
||||
<small class="text-muted d-block">State: @warning.PreviousKnowledgeState → @warning.CurrentKnowledgeState</small>
|
||||
}
|
||||
@if (warning.PreviousSceneId.HasValue)
|
||||
{
|
||||
<small class="text-muted d-block">
|
||||
Previous:
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@warning.PreviousSceneId">@warning.PreviousSceneDisplayName</a>
|
||||
</small>
|
||||
}
|
||||
@if (warning.CurrentSceneId.HasValue)
|
||||
{
|
||||
<small class="text-muted d-block">
|
||||
Current:
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@warning.CurrentSceneId">@warning.CurrentSceneDisplayName</a>
|
||||
</small>
|
||||
}
|
||||
@if (warning.EstablishedSceneId.HasValue)
|
||||
{
|
||||
<small class="text-muted d-block">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user