Investigate and fix issues discovered after Phase 12E Continuity Warnings.

This commit is contained in:
Nick Beckley 2026-06-14 20:22:24 +01:00
parent 7c7815642a
commit 279c8f182c
2 changed files with 58 additions and 46 deletions

View File

@ -2766,14 +2766,14 @@ public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : I
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<AssetCustodyEvent>("dbo.AssetCustodyEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
return await WithValidCustodianSummariesAsync(connection, rows.ToList());
}
public async Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyByAssetAsync(int storyAssetId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<AssetCustodyEvent>("dbo.AssetCustodyEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
return await WithValidCustodianSummariesAsync(connection, rows.ToList());
}
public async Task<IReadOnlyList<AssetCustodyCharacter>> 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<IReadOnlyList<AssetCustodyEvent>> WithValidCustodianSummariesAsync(IDbConnection connection, List<AssetCustodyEvent> rows)
{
if (rows.Count == 0)
{
return rows;
}
var eventIds = rows.Select(x => x.AssetCustodyEventID).Distinct().ToList();
var validCustodians = await connection.QueryAsync<AssetCustodySummaryRow>(
"""
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<int> SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable<int> custodianCharacterIds, int custodyRoleId)
{
using var connection = connectionFactory.CreateConnection();

View File

@ -2804,46 +2804,10 @@ public sealed class StoryStateService(
IReadOnlySet<int> enabledCharacterIds,
RangeContext context)
{
var warnings = new List<ContinuityWarningViewModel>();
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<ContinuityWarningViewModel> 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;
}