Phase 12G: Knowledge Continuity Warnings.
This commit is contained in:
parent
738b9f496d
commit
e008da0cf4
@ -2546,6 +2546,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));
|
||||
}
|
||||
|
||||
if (filter.IncludeAssets)
|
||||
@ -2565,7 +2566,7 @@ public sealed class StoryStateService(
|
||||
.ThenBy(x => x.BookDisplayName)
|
||||
.ThenBy(x => x.ChapterDisplayName)
|
||||
.ThenBy(x => x.SceneID ?? int.MaxValue)
|
||||
.ThenBy(x => x.WarningType)
|
||||
.ThenBy(x => x.WarningTypeDisplayName)
|
||||
.ThenBy(x => x.Message)
|
||||
.ToList();
|
||||
}
|
||||
@ -2976,6 +2977,79 @@ public sealed class StoryStateService(
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<ContinuityWarningViewModel>> BuildKnowledgeChronologyWarningsAsync(
|
||||
IReadOnlyList<Character> projectCharacters,
|
||||
IReadOnlySet<int> enabledCharacterIds,
|
||||
RangeContext context)
|
||||
{
|
||||
var warnings = new List<ContinuityWarningViewModel>();
|
||||
var charactersToCheck = projectCharacters
|
||||
.Where(character => !enabledCharacterIds.Any() || enabledCharacterIds.Contains(character.CharacterID))
|
||||
.ToList();
|
||||
|
||||
foreach (var character in charactersToCheck)
|
||||
{
|
||||
var knowledgeEvents = (await characters.ListKnowledgeByCharacterAsync(character.CharacterID))
|
||||
.Where(x => context.SceneIndexes.ContainsKey(x.SceneID))
|
||||
.OrderBy(x => context.SceneIndexes.GetValueOrDefault(x.SceneID, int.MaxValue))
|
||||
.ThenBy(x => x.CreatedDate)
|
||||
.ThenBy(x => x.CharacterKnowledgeID)
|
||||
.ToList();
|
||||
|
||||
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())
|
||||
.ToList();
|
||||
|
||||
foreach (var reference in earlierReferences)
|
||||
{
|
||||
var warning = BuildWarning(
|
||||
$"Knowledge Chronology Violation|K:{established.CharacterKnowledgeID}",
|
||||
"Warning",
|
||||
$"{character.CharacterName} appears to know \"{KnowledgeTitle(established)}\" before this knowledge is established.",
|
||||
reference.SceneID,
|
||||
context,
|
||||
characterId: character.CharacterID);
|
||||
|
||||
warning.WarningTypeDisplayName = "Knowledge Chronology Violation";
|
||||
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));
|
||||
warnings.Add(warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warnings
|
||||
.GroupBy(x => new { x.CharacterID, x.KnowledgeId, x.ReferencedSceneId, x.EstablishedSceneId })
|
||||
.Select(x => x.First())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static LocationActivityEntityViewModel? BuildCurrentCharacterPresence(
|
||||
Character character,
|
||||
IReadOnlyList<SceneCharacter> assignments,
|
||||
@ -3143,6 +3217,7 @@ public sealed class StoryStateService(
|
||||
{
|
||||
WarningKey = BuildWarningKey(warningType, sceneId, characterId, assetId),
|
||||
WarningType = warningType,
|
||||
WarningTypeDisplayName = warningType,
|
||||
Severity = severity,
|
||||
Message = message,
|
||||
CharacterID = characterId,
|
||||
@ -3154,6 +3229,36 @@ public sealed class StoryStateService(
|
||||
};
|
||||
}
|
||||
|
||||
private static string KnowledgeIdentity(CharacterKnowledgeItem item)
|
||||
{
|
||||
if (item.StoryAssetID.HasValue)
|
||||
{
|
||||
return $"Asset:{item.StoryAssetID.Value}";
|
||||
}
|
||||
|
||||
if (item.PlotThreadID.HasValue)
|
||||
{
|
||||
return $"Thread:{item.PlotThreadID.Value}";
|
||||
}
|
||||
|
||||
return $"Description:{(item.Description ?? string.Empty).Trim().ToUpperInvariant()}";
|
||||
}
|
||||
|
||||
private static string KnowledgeTitle(CharacterKnowledgeItem item)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(item.AssetName))
|
||||
{
|
||||
return item.AssetName;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.ThreadTitle))
|
||||
{
|
||||
return item.ThreadTitle;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(item.Description) ? "Knowledge" : item.Description.Trim();
|
||||
}
|
||||
|
||||
private static void ApplyAcknowledgements(
|
||||
IReadOnlyList<ContinuityWarningViewModel> warnings,
|
||||
IReadOnlyList<ContinuityWarningAcknowledgement> acknowledgements)
|
||||
|
||||
@ -319,9 +319,11 @@ public sealed class ContinuityWarningViewModel
|
||||
{
|
||||
public string WarningKey { get; set; } = string.Empty;
|
||||
public string WarningType { get; set; } = string.Empty;
|
||||
public string WarningTypeDisplayName { get; set; } = string.Empty;
|
||||
public string Severity { get; set; } = "Information";
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public int? CharacterID { get; set; }
|
||||
public string CharacterName { get; set; } = string.Empty;
|
||||
public int? AssetID { get; set; }
|
||||
public int? SceneID { get; set; }
|
||||
public string SceneDisplayName { get; set; } = string.Empty;
|
||||
@ -329,6 +331,12 @@ public sealed class ContinuityWarningViewModel
|
||||
public string ChapterDisplayName { get; set; } = string.Empty;
|
||||
public bool IsAcknowledged { get; set; }
|
||||
public string? AcknowledgementNotes { get; set; }
|
||||
public int? KnowledgeId { get; set; }
|
||||
public string KnowledgeTitle { get; set; } = string.Empty;
|
||||
public int? EstablishedSceneId { get; set; }
|
||||
public string EstablishedSceneDisplayName { get; set; } = string.Empty;
|
||||
public int? ReferencedSceneId { get; set; }
|
||||
public string ReferencedSceneDisplayName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class CharacterJourneyViewModel
|
||||
|
||||
@ -382,7 +382,11 @@ else if (Model.Filter.DisplayMode == "ContinuityWarnings")
|
||||
<tr>
|
||||
<th>Severity</th>
|
||||
<th>Status</th>
|
||||
<th>Warning Type</th>
|
||||
<th>Type</th>
|
||||
<th>Character</th>
|
||||
<th>Knowledge</th>
|
||||
<th>Established</th>
|
||||
<th>Earlier Reference</th>
|
||||
<th>Message</th>
|
||||
<th>Scene</th>
|
||||
<th>Actions</th>
|
||||
@ -403,7 +407,47 @@ else if (Model.Filter.DisplayMode == "ContinuityWarnings")
|
||||
<span class="badge bg-warning text-dark">Active</span>
|
||||
}
|
||||
</td>
|
||||
<td>@warning.WarningType</td>
|
||||
<td>@warning.WarningTypeDisplayName</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrWhiteSpace(warning.CharacterName))
|
||||
{
|
||||
@warning.CharacterName
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrWhiteSpace(warning.KnowledgeTitle))
|
||||
{
|
||||
@warning.KnowledgeTitle
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (warning.EstablishedSceneId.HasValue)
|
||||
{
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@warning.EstablishedSceneId">@warning.EstablishedSceneDisplayName</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (warning.ReferencedSceneId.HasValue)
|
||||
{
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@warning.ReferencedSceneId">@warning.ReferencedSceneDisplayName</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div>@warning.Message</div>
|
||||
@if (warning.IsAcknowledged && !string.IsNullOrWhiteSpace(warning.AcknowledgementNotes))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user