From 7001359281cebf011d51b433a1c1d0307b6826bc Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Mon, 13 Jul 2026 21:15:49 +0000 Subject: [PATCH] Phase 21M - Deterministic Illustration Assignment Repair --- PlotLine.Tests/Program.cs | 41 ++++++++++- ...ryIntelligenceIllustrationCompatibility.cs | 68 ++++++++++++++++--- ...IntelligenceIllustrationMatchingService.cs | 51 ++++++++++++-- ...ntelligenceVisualisationSnapshotService.cs | 6 +- ...telligenceExperiencePrototypeViewModels.cs | 4 ++ ...IntelligenceIllustrationDiagnostics.cshtml | 30 ++++++++ .../prototype/character-group.svg | 20 ++++++ 7 files changed, 201 insertions(+), 19 deletions(-) create mode 100644 PlotLine/wwwroot/images/story-intelligence/prototype/character-group.svg diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index f10d5bd..355e25b 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -47,7 +47,8 @@ var tests = new (string Name, Action Test)[] ("Illustration matching rejects duplicate named portrait allocation", IllustrationMatchingRejectsDuplicateNamedPortraitAllocation), ("Illustration semantic types prevent bathroom office and ambulance car mismatches", IllustrationSemanticTypesPreventObviousMismatches), ("Illustration demand archetypes stay broad and use active template", IllustrationDemandArchetypesStayBroadAndUseActiveTemplate), - ("Story Intelligence evidence extraction prevents observed attribute leakage", StoryIntelligenceEvidenceExtractionPreventsObservedAttributeLeakage) + ("Story Intelligence evidence extraction prevents observed attribute leakage", StoryIntelligenceEvidenceExtractionPreventsObservedAttributeLeakage), + ("Illustration assignment repair enforces explicit evidence groups and strict subtypes", IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictSubtypes) }; foreach (var test in tests) @@ -347,13 +348,13 @@ static void IllustrationPromptBuilderSeparatesSpecAndPrompt() var result = builder.Build(spec); Assert(result.TemplateVersion == IllustrationPromptBuilder.CurrentTemplateVersion, "Template version was not recorded."); - Assert(result.TemplateVersion == "21D.2", "Illustration prompt template version should reflect the vivid art-direction reset."); + Assert(result.TemplateVersion == "21J.3", "Illustration prompt template version should reflect the current approved art direction."); Assert(result.Prompt.Contains("no visible text", StringComparison.OrdinalIgnoreCase), "Prompt did not include visual safety guidance."); Assert(result.Prompt.Contains("Category composition rules", StringComparison.Ordinal), "Prompt did not include category composition rules."); Assert(result.Prompt.Contains("Structured details", StringComparison.Ordinal), "Prompt did not include structured details."); Assert(result.Prompt.Contains("modern cinematic digital illustration", StringComparison.OrdinalIgnoreCase), "Prompt did not include the new cinematic digital art direction."); Assert(result.Prompt.Contains("premium narrative game concept art", StringComparison.OrdinalIgnoreCase), "Prompt did not include premium game concept-art guidance."); - Assert(result.Prompt.Contains("bright balanced lighting", StringComparison.OrdinalIgnoreCase), "Prompt did not include brighter lighting guidance."); + Assert(result.Prompt.Contains("balanced cinematic lighting", StringComparison.OrdinalIgnoreCase), "Prompt did not include current lighting guidance."); Assert(result.Prompt.Contains("avoid muddy olive and brown grading", StringComparison.OrdinalIgnoreCase), "Prompt did not explicitly avoid the previous muddy grading failure mode."); Assert(result.Prompt.Contains(spec.Code, StringComparison.Ordinal), "Prompt did not include stable code."); Assert(result.SpecificationJson.Contains("\"code\": \"asset-folded-letter\"", StringComparison.Ordinal), "Specification JSON did not preserve the stable code."); @@ -560,6 +561,40 @@ static void IllustrationDemandArchetypesStayBroadAndUseActiveTemplate() Assert(IllustrationPromptBuilder.CurrentTemplateVersion == "21J.3", $"Demand generation should use the active shared template, got {IllustrationPromptBuilder.CurrentTemplateVersion}."); } +static void IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictSubtypes() +{ + var femaleInstructor = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("female instructor", ["female instructor"], []); + var maleExaminers = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("younger male examiners", ["younger male examiners"], []); + var lad = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("tubby lad", ["tubby lad"], []); + var girl = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("girl", ["schoolgirl"], []); + var kevin = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Kevin", ["Kevin", "female instructor (Kevin's)"], ["female instructor", "Kevin's examiner"]); + + Assert(femaleInstructor.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"female instructor should be Feminine, got {femaleInstructor.Presentation}."); + Assert(maleExaminers.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"male examiners should be Masculine, got {maleExaminers.Presentation}."); + Assert(lad.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"lad should be Masculine, got {lad.Presentation}."); + Assert(lad.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Child, $"lad should infer a young age band, got {lad.AgeBand}."); + Assert(girl.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"girl should be Feminine, got {girl.Presentation}."); + Assert(girl.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Child, $"schoolgirl should infer Child, got {girl.AgeBand}."); + Assert(kevin.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"Kevin should not inherit feminine presentation from his instructor, got {kevin.Presentation}."); + Assert(StoryIntelligenceIllustrationCompatibility.IsGroupEntity("social workers"), "Social workers should be treated as a group entity."); + Assert(StoryIntelligenceIllustrationCompatibility.IsGroupEntity("younger male examiners"), "Plural examiners should be treated as a group entity."); + Assert(StoryIntelligenceIllustrationCompatibility.IsGroupEntity("uniformed people"), "Uniformed people should be treated as a group entity."); + Assert(!StoryIntelligenceIllustrationCompatibility.IsGroupEntity("social worker"), "Singular social worker should remain an individual entity."); + + Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("Bathroom", "LaundryUtilityRoom"), "Bathroom must not match laundry room."); + Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("Road", "CarPark"), "Road must not match car park."); + Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("WaitingRoom", "FlatInterior"), "Waiting room must not match flat interior."); + Assert(StoryIntelligenceIllustrationCompatibility.AssetType("Ford Fiesta") == "Car", "Ford Fiesta should classify as Car."); + Assert(StoryIntelligenceIllustrationCompatibility.AssetType("Car") == "Car", "Car should classify as Car."); + Assert(StoryIntelligenceIllustrationCompatibility.AssetType("supercharger") == "CarPart", "Supercharger should not classify as full car."); + Assert(StoryIntelligenceIllustrationCompatibility.AssetType("biscuit tin") == "Tin", "Biscuit tin should not classify as Photograph."); + Assert(StoryIntelligenceIllustrationCompatibility.AssetType("parcel") == "Parcel", "Parcel should not classify as Letter."); + + var beth = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Beth", ["Beth is fifteen."], []); + var adult = Metadata("char-family-relative", "MatureAdult", "Feminine"); + Assert(Rejected(beth, adult).Any(reason => reason.Contains("age hard mismatch", StringComparison.OrdinalIgnoreCase)), "Existing adult assignment must be discarded when Beth evidence says YoungTeen."); +} + static IllustrationCharacterMetadata Metadata(string code, string ageBand, string presentation, bool isUnknownFigure = false) => new( ageBand, diff --git a/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs b/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs index a9d5aab..0a9dcdb 100644 --- a/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs +++ b/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs @@ -48,11 +48,12 @@ public static class StoryIntelligenceIllustrationCompatibility public static CharacterEvidenceProfile InferCharacterEvidence(string name, IEnumerable identityValues, IEnumerable relationshipValues) { var identityText = $" {string.Join(' ', identityValues.Where(value => !string.IsNullOrWhiteSpace(value))).ToLowerInvariant()} "; - var relationshipText = $" {string.Join(' ', relationshipValues.Where(value => !string.IsNullOrWhiteSpace(value))).ToLowerInvariant()} "; + var identityScopedText = $" {string.Join(". ", identityValues.Where(value => !string.IsNullOrWhiteSpace(value))).ToLowerInvariant()} "; + var relationshipText = $" {string.Join(". ", relationshipValues.Where(value => !string.IsNullOrWhiteSpace(value))).ToLowerInvariant()} "; var nameText = $" {name.ToLowerInvariant()} "; var entityText = $"{nameText} {identityText}"; - var scopedText = EntityScopedText(entityText, name); - var relationshipSafeText = $"{nameText} {relationshipText}"; + var scopedText = EntityScopedText($"{nameText} {identityScopedText}", name); + var relationshipSafeText = RelationshipScopedText(relationshipText, name); var titlePresentation = TitlePresentation(scopedText); var relationshipPresentation = RelationshipPresentation(relationshipSafeText); var entityPresentation = RelationshipPresentation(scopedText); @@ -251,6 +252,7 @@ public static class StoryIntelligenceIllustrationCompatibility public static string AssetType(string? value) { var text = (value ?? string.Empty).ToLowerInvariant(); + var padded = $" {text} "; if (text.Contains("ambulance", StringComparison.OrdinalIgnoreCase)) return "Ambulance"; if (text.Contains("stretcher", StringComparison.OrdinalIgnoreCase)) return "Stretcher"; if (text.Contains("body bag", StringComparison.OrdinalIgnoreCase)) return "BodyBag"; @@ -260,6 +262,10 @@ public static class StoryIntelligenceIllustrationCompatibility if (ContainsAny(text, "driving licence", "driver licence", "license", "licence")) return "Document"; if (ContainsAny(text, "ring", "necklace", "jewellery", "jewelry")) return "Jewellery"; if (ContainsAny(text, "gun", "pistol", "rifle", "weapon")) return "Weapon"; + if (ContainsAny(padded, " ford fiesta ", " fiesta ", " motor car ", " car ", " vehicle ")) return "Car"; + if (ContainsAny(text, "supercharger", "engine part")) return "CarPart"; + if (ContainsAny(text, "biscuit tin", "tin")) return "Tin"; + if (ContainsAny(text, "parcel", "package")) return "Parcel"; if (text.Contains("rucksack", StringComparison.OrdinalIgnoreCase)) return "Rucksack"; if (text.Contains("suitcase", StringComparison.OrdinalIgnoreCase)) return "Suitcase"; if (text.Contains("note", StringComparison.OrdinalIgnoreCase)) return "Note"; @@ -268,7 +274,7 @@ public static class StoryIntelligenceIllustrationCompatibility if (text.Contains("book", StringComparison.OrdinalIgnoreCase)) return "Book"; if (ContainsAny(text, "telephone", "phone", "recorder")) return "Telephone"; if (text.Contains("key", StringComparison.OrdinalIgnoreCase)) return "Keys"; - if (ContainsAny(text, "tr6", " car ", "vehicle")) return "Car"; + if (ContainsAny(text, "tr6")) return "Car"; if (ContainsAny(text, "photo", "photograph")) return "Photograph"; if (ContainsAny(text, "document", "paper")) return "Document"; return "UnknownObject"; @@ -277,7 +283,7 @@ public static class StoryIntelligenceIllustrationCompatibility public static bool LocationCompatible(string requestedType, string candidateType) => requestedType == candidateType || (requestedType is "FrontDoor" or "Porch" && candidateType is "HouseExterior") - || (requestedType is "Kitchen" or "LivingRoom" or "Bedroom" or "WaitingRoom" && candidateType is "FlatInterior") + || (requestedType is "Kitchen" or "LivingRoom" or "Bedroom" && candidateType is "FlatInterior") || (requestedType == "Bathroom" && candidateType == "Bathroom"); public static bool AssetCompatible(string requestedType, string candidateType) @@ -285,12 +291,29 @@ public static class StoryIntelligenceIllustrationCompatibility || (requestedType == "Note" && candidateType is "Letter" or "Document") || (requestedType == "Rucksack" && candidateType == "Rucksack"); + public static bool IsGroupEntity(string? value) + { + var clean = (value ?? string.Empty).Trim().ToLowerInvariant(); + return clean.EndsWith('s') + && (clean.Contains("social worker", StringComparison.Ordinal) + || clean.Contains("neighbour", StringComparison.Ordinal) + || clean.Contains("neighbor", StringComparison.Ordinal) + || clean.Contains("police officer", StringComparison.Ordinal) + || clean.Contains("student", StringComparison.Ordinal) + || clean.Contains("examiner", StringComparison.Ordinal) + || clean.Contains("uniformed", StringComparison.Ordinal) + || clean.Contains("responder", StringComparison.Ordinal)) + || clean is "crowd" or "uniformed people" or "police" or "social services"; + } + private static EvidenceSignal ExplicitAgeBand(string text) { if (ContainsAny(text, "15", "fifteen")) return new(AgeBands.YoungTeen, 0.98m); if (ContainsAny(text, "13", "thirteen", "14", "fourteen")) return new(AgeBands.YoungTeen, 0.98m); if (ContainsAny(text, "16", "sixteen", "17", "seventeen", "18", "eighteen", "19", "nineteen")) return new(AgeBands.OlderTeen, 0.98m); - if (ContainsAny(text, "child", "young girl", "young boy")) return new(AgeBands.Child, 0.86m); + if (ContainsAny(text, "toddler", "baby", "infant", "newborn")) return new(AgeBands.Child, 0.98m); + if (ContainsAny(text, "child", "young girl", "young boy", "schoolboy", "schoolgirl", " boy ", " girl ", " lad ")) return new(AgeBands.Child, 0.86m); + if (ContainsAny(text, "teenager", "teenage")) return new(AgeBands.OlderTeen, 0.86m); if (ContainsAny(text, "young adult", "young woman", "young man")) return new(AgeBands.YoungAdult, 0.82m); if (ContainsAny(text, "middle-aged", "middle aged", "mature adult")) return new(AgeBands.MatureAdult, 0.86m); if (ContainsAny(text, "elderly", "senior", "old woman", "old man")) return new(AgeBands.Senior, 0.9m); @@ -327,8 +350,8 @@ public static class StoryIntelligenceIllustrationCompatibility private static EvidenceSignal RelationshipPresentation(string text) { - if (ContainsAny(text, "mother", " mum", "mummy", "aunt", "sister", "daughter", "wife", "grandmother", " niece", "woman", "girl", "lady", "queen")) return new(Presentations.Feminine, 0.92m); - if (ContainsAny(text, "father", " dad", "daddy", "uncle", "brother", "son", "husband", "grandfather", " nephew", "man", "boy", "lad", "king")) return new(Presentations.Masculine, 0.92m); + if (ContainsAny(text, "female", "mother", " mum", "mummy", "aunt", "sister", "daughter", "wife", "widow", "grandmother", " niece", "woman", "girl", "lady", "queen")) return new(Presentations.Feminine, 0.92m); + if (ContainsAny(text, "male", "gentleman", "father", " dad", "daddy", "uncle", "brother", "son", "husband", "widower", "grandfather", " nephew", "man", "boy", "lad", "bloke", "chap", "king")) return new(Presentations.Masculine, 0.92m); return EvidenceSignal.Unknown; } @@ -413,10 +436,39 @@ public static class StoryIntelligenceIllustrationCompatibility } var sentences = text.Split(['.', '!', '?', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var possessiveAscii = $"{cleanName}'s"; + var possessiveCurly = $"{cleanName}\u2019s"; var scoped = sentences.Where(sentence => sentence.Contains(cleanName, StringComparison.OrdinalIgnoreCase)).ToList(); + scoped = scoped + .Where(sentence => !sentence.Contains(possessiveAscii, StringComparison.OrdinalIgnoreCase) + && !sentence.Contains(possessiveCurly, StringComparison.OrdinalIgnoreCase)) + .ToList(); return scoped.Count == 0 ? $" {cleanName} " : $" {string.Join(' ', scoped)} "; } + private static string RelationshipScopedText(string text, string entityName) + { + var cleanName = (entityName ?? string.Empty).Trim().ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(cleanName)) + { + return text; + } + + var possessiveAscii = $"{cleanName}'s"; + var possessiveCurly = $"{cleanName}\u2019s"; + if (text.Contains(possessiveAscii, StringComparison.OrdinalIgnoreCase) + || text.Contains(possessiveCurly, StringComparison.OrdinalIgnoreCase)) + { + return string.Empty; + } + + var fragments = text.Split(['.', '!', '?', ';', ',', '|'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(fragment => !fragment.Contains(possessiveAscii, StringComparison.OrdinalIgnoreCase) + && !fragment.Contains(possessiveCurly, StringComparison.OrdinalIgnoreCase)) + .ToList(); + return fragments.Count == 0 ? string.Empty : $" {string.Join(' ', fragments)} "; + } + private static bool AttributeNearEntity(string sentence, string cleanName) { var lower = sentence.ToLowerInvariant(); diff --git a/PlotLine/Services/StoryIntelligenceIllustrationMatchingService.cs b/PlotLine/Services/StoryIntelligenceIllustrationMatchingService.cs index bcbd4ba..7d18bd9 100644 --- a/PlotLine/Services/StoryIntelligenceIllustrationMatchingService.cs +++ b/PlotLine/Services/StoryIntelligenceIllustrationMatchingService.cs @@ -68,6 +68,12 @@ public sealed class StoryIntelligenceIllustrationMatchingService( foreach (var observation in observations.Values.OrderByDescending(item => item.Significance).ThenBy(item => item.Name)) { + if (StoryIntelligenceIllustrationCompatibility.IsGroupEntity(observation.Name)) + { + ApplyGroupDiagnostics(prototype, observation); + continue; + } + var existing = existingAssignments.GetValueOrDefault(observation.CharacterKey); var ownExistingItemId = existing?.IllustrationLibraryItemID; if (ownExistingItemId.HasValue && duplicateExistingItemIds.Contains(ownExistingItemId.Value) && !retainedExistingItemIds.Add(ownExistingItemId.Value)) @@ -119,10 +125,12 @@ public sealed class StoryIntelligenceIllustrationMatchingService( var reasonReassigned = existingScore is not null && chosen.Candidate.Item.IllustrationLibraryItemID != existing?.IllustrationLibraryItemID ? ReassignmentReason(observation, existingScore, chosen) - : null; + : existing is { IllustrationLibraryItemID: null } + ? $"Assigned {chosen.Candidate.Item.StableCode} because generated demand or newly valid library art replaced a fallback assignment." + : null; var reason = reasonReassigned ?? (existing is null ? $"Assigned {chosen.Candidate.Item.StableCode} from structured character evidence." - : "Preserved existing Story Intelligence character illustration assignment."); + : $"Retained {chosen.Candidate.Item.StableCode}; existing assignment passed hard revalidation with score {chosen.Score:N0}."); var candidateDiagnostics = JsonSerializer.Serialize(allScores.Take(6).Select(MatchDiagnostics.From), JsonOptions); var evidenceJson = JsonSerializer.Serialize(observation.Evidence, JsonOptions); @@ -140,7 +148,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService( reasonReassigned)); assignedItemIds.Add(chosen.Candidate.Item.IllustrationLibraryItemID); - ApplyAssignment(prototype, observation, chosen, saved, reason, reasonReassigned, candidateDiagnostics); + ApplyAssignment(prototype, observation, existing, chosen, saved, reason, reasonReassigned, evidenceJson, candidateDiagnostics); } } @@ -159,7 +167,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService( if (best is null) { - return existingScore; + return existingScore.Score >= SuitableScore ? existingScore : null; } var materiallyWrong = MateriallyWrong(existingScore.Candidate.Metadata, observation.Evidence); @@ -170,7 +178,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService( if (!materiallyWrong || best.Score < existingScore.Score + ReassignmentMargin) { - return existingScore; + return existingScore.Score >= SuitableScore ? existingScore : best; } } @@ -441,10 +449,12 @@ public sealed class StoryIntelligenceIllustrationMatchingService( private void ApplyAssignment( StoryIntelligenceExperiencePrototypeViewModel prototype, CharacterObservation observation, + StoryIntelligenceCharacterIllustrationAssignment? existing, MatchScore chosen, StoryIntelligenceCharacterIllustrationAssignment saved, string reason, string? reasonReassigned, + string evidenceJson, string candidateDiagnostics) { foreach (var character in prototype.Scenes.SelectMany(scene => scene.Characters).Where(character => string.Equals(character.Id, observation.CharacterKey, StringComparison.OrdinalIgnoreCase))) @@ -468,6 +478,10 @@ public sealed class StoryIntelligenceIllustrationMatchingService( RejectedCandidates = candidateDiagnostics, ReasonChosen = reason, ReasonReassigned = reasonReassigned, + PreviousIllustration = existing?.StableCode, + PreviousEvidence = existing?.EvidenceJson, + CurrentEvidence = evidenceJson, + DemandKey = DemandKey(observation.Evidence), DemandStatus = "Satisfied", ResolvedPresentation = observation.Evidence.Presentation, ResolvedAgeBand = observation.Evidence.AgeBand, @@ -506,6 +520,8 @@ public sealed class StoryIntelligenceIllustrationMatchingService( MatchingScore = allScores.FirstOrDefault()?.Score, RejectedCandidates = diagnostics, ReasonChosen = "No suitable unused illustration matched structured character evidence.", + CurrentEvidence = JsonSerializer.Serialize(observation.Evidence, JsonOptions), + DemandKey = DemandKey(observation.Evidence), DemandStatus = demandStatus, ResolvedPresentation = observation.Evidence.Presentation, ResolvedAgeBand = observation.Evidence.AgeBand, @@ -552,6 +568,31 @@ public sealed class StoryIntelligenceIllustrationMatchingService( || clean.StartsWith("unidentified ", StringComparison.Ordinal); } + private static void ApplyGroupDiagnostics(StoryIntelligenceExperiencePrototypeViewModel prototype, CharacterObservation observation) + { + foreach (var character in prototype.Scenes.SelectMany(scene => scene.Characters).Where(character => string.Equals(character.Id, observation.CharacterKey, StringComparison.OrdinalIgnoreCase))) + { + character.LibraryCode = string.Empty; + character.ImagePath = "/images/story-intelligence/prototype/character-group.svg"; + character.ImageResolution = new StoryIntelligenceExperienceImageResolution + { + EntityId = character.Id, + StableCode = string.Empty, + IllustrationLibraryItemId = null, + Status = "GroupEntity", + FinalImageUrl = null, + FallbackImageUrl = character.ImagePath, + FallbackUsed = true, + ReasonChosen = "Collective group entity; reusable group fallback used without unique portrait assignment or demand.", + CurrentEvidence = JsonSerializer.Serialize(observation.Evidence, JsonOptions), + DemandStatus = "Not required for group entity", + ResolvedPresentation = "Unknown", + ResolvedAgeBand = "Unknown", + IllustrationAllocationStatus = "Group entity" + }; + } + } + private static string NormaliseUnknown(string? value, string fallback) => IsUnknown(value) ? fallback : value!; diff --git a/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs b/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs index e5373c7..73426f8 100644 --- a/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs +++ b/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs @@ -1760,11 +1760,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService( { var value = $"{name} {location?.LocationType} {location?.GenericRoomType} {setting?.LocationType}".ToLowerInvariant(); var type = StoryIntelligenceIllustrationCompatibility.LocationType(value); - if (type == "Bathroom") return "loc-laundry-utility-room"; - if (type is "Kitchen" or "Bedroom" or "LivingRoom" or "WaitingRoom" or "FlatInterior" or "Hallway") return "loc-small-flat-interior"; + if (type is "Bathroom" or "WaitingRoom" or "Road") return string.Empty; + if (type is "Kitchen" or "Bedroom" or "LivingRoom" or "FlatInterior" or "Hallway") return "loc-small-flat-interior"; if (type == "Office") return "loc-domestic-desk"; if (type == "Stairwell") return "loc-narrow-stairwell"; - if (type is "CarPark" or "Road") return "loc-wet-car-park"; + if (type is "CarPark") return "loc-wet-car-park"; if (type == "HouseExterior") return "loc-estate-house-exterior"; return string.Empty; } diff --git a/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs b/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs index 4f1b9e6..2b16007 100644 --- a/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs +++ b/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs @@ -151,6 +151,10 @@ public sealed class StoryIntelligenceExperienceImageResolution public string? RejectedCandidates { get; init; } public string? ReasonChosen { get; init; } public string? ReasonReassigned { get; init; } + public string? PreviousIllustration { get; init; } + public string? PreviousEvidence { get; init; } + public string? CurrentEvidence { get; init; } + public string? DemandKey { get; init; } public string? DemandStatus { get; init; } public string? ResolvedPresentation { get; init; } public string? ResolvedAgeBand { get; init; } diff --git a/PlotLine/Views/Development/StoryIntelligenceIllustrationDiagnostics.cshtml b/PlotLine/Views/Development/StoryIntelligenceIllustrationDiagnostics.cshtml index 3b8a513..49892a0 100644 --- a/PlotLine/Views/Development/StoryIntelligenceIllustrationDiagnostics.cshtml +++ b/PlotLine/Views/Development/StoryIntelligenceIllustrationDiagnostics.cshtml @@ -24,8 +24,11 @@ Identity Illustration Resolved evidence + Revalidation Assignment + Previous / change Demand + Demand key Candidate diagnostics @@ -37,8 +40,19 @@ @row.Character.Name
@row.Character.Id @(row.Character.ImageResolution.StableCode ?? row.Character.LibraryCode)
@row.Character.ImageResolution.Status Age @Display(row.Character.ImageResolution.ResolvedAgeBand)
Presentation @Display(row.Character.ImageResolution.ResolvedPresentation) + @Revalidation(row.Character.ImageResolution) @Display(row.Character.ImageResolution.ReasonChosen)
@Display(row.Character.ImageResolution.IllustrationAllocationStatus) + + Previous illustration: @Display(row.Character.ImageResolution.PreviousIllustration)
+ Change: @Display(row.Character.ImageResolution.ReasonReassigned)
+
+ Evidence +
Previous: @Display(row.Character.ImageResolution.PreviousEvidence)
+Current: @Display(row.Character.ImageResolution.CurrentEvidence)
+
+ @Display(row.Character.ImageResolution.DemandStatus) + @Display(row.Character.ImageResolution.DemandKey)
@Display(row.Character.ImageResolution.RejectedCandidates)
} @@ -116,4 +130,20 @@ @functions { private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "none" : value; + + private static string Revalidation(StoryIntelligenceExperienceImageResolution resolution) + { + if (string.Equals(resolution.Status, "GroupEntity", StringComparison.OrdinalIgnoreCase)) + { + return "Group entity"; + } + + if (resolution.FallbackUsed) + { + return "No compatible illustration"; + } + + return string.IsNullOrWhiteSpace(resolution.ReasonReassigned) ? "Passed" : "Reassigned"; + } + } diff --git a/PlotLine/wwwroot/images/story-intelligence/prototype/character-group.svg b/PlotLine/wwwroot/images/story-intelligence/prototype/character-group.svg new file mode 100644 index 0000000..9119782 --- /dev/null +++ b/PlotLine/wwwroot/images/story-intelligence/prototype/character-group.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + +