Phase 21M - Deterministic Illustration Assignment Repair

This commit is contained in:
Nick Beckley 2026-07-13 21:15:49 +00:00
parent 948ae2e54d
commit 7001359281
7 changed files with 201 additions and 19 deletions

View File

@ -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,

View File

@ -48,11 +48,12 @@ public static class StoryIntelligenceIllustrationCompatibility
public static CharacterEvidenceProfile InferCharacterEvidence(string name, IEnumerable<string> identityValues, IEnumerable<string> 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();

View File

@ -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)
: 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!;

View File

@ -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;
}

View File

@ -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; }

View File

@ -24,8 +24,11 @@
<th>Identity</th>
<th>Illustration</th>
<th>Resolved evidence</th>
<th>Revalidation</th>
<th>Assignment</th>
<th>Previous / change</th>
<th>Demand</th>
<th>Demand key</th>
<th>Candidate diagnostics</th>
</tr>
</thead>
@ -37,8 +40,19 @@
<td><strong>@row.Character.Name</strong><br /><span class="muted">@row.Character.Id</span></td>
<td><code>@(row.Character.ImageResolution.StableCode ?? row.Character.LibraryCode)</code><br />@row.Character.ImageResolution.Status</td>
<td>Age @Display(row.Character.ImageResolution.ResolvedAgeBand)<br />Presentation @Display(row.Character.ImageResolution.ResolvedPresentation)</td>
<td>@Revalidation(row.Character.ImageResolution)</td>
<td>@Display(row.Character.ImageResolution.ReasonChosen)<br /><span class="muted">@Display(row.Character.ImageResolution.IllustrationAllocationStatus)</span></td>
<td>
Previous illustration: <code>@Display(row.Character.ImageResolution.PreviousIllustration)</code><br />
Change: @Display(row.Character.ImageResolution.ReasonReassigned)<br />
<details>
<summary>Evidence</summary>
<pre class="small mb-0">Previous: @Display(row.Character.ImageResolution.PreviousEvidence)
Current: @Display(row.Character.ImageResolution.CurrentEvidence)</pre>
</details>
</td>
<td>@Display(row.Character.ImageResolution.DemandStatus)</td>
<td><code>@Display(row.Character.ImageResolution.DemandKey)</code></td>
<td><pre class="small mb-0">@Display(row.Character.ImageResolution.RejectedCandidates)</pre></td>
</tr>
}
@ -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";
}
}

View File

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-label="Group of people">
<defs>
<linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#18283d"/>
<stop offset="1" stop-color="#07131f"/>
</linearGradient>
<linearGradient id="ring" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#8fb9df"/>
<stop offset="1" stop-color="#d9a85f"/>
</linearGradient>
</defs>
<rect width="240" height="240" rx="120" fill="url(#bg)"/>
<circle cx="120" cy="120" r="86" fill="none" stroke="url(#ring)" stroke-width="6" opacity=".85"/>
<circle cx="120" cy="92" r="26" fill="#d8e5f1" opacity=".54"/>
<path d="M74 164c8-31 28-48 46-48s38 17 46 48" fill="#d8e5f1" opacity=".32"/>
<circle cx="76" cy="112" r="20" fill="#8fb9df" opacity=".38"/>
<path d="M42 166c7-25 21-38 34-38 11 0 22 8 29 24" fill="#8fb9df" opacity=".2"/>
<circle cx="164" cy="112" r="20" fill="#d9a85f" opacity=".42"/>
<path d="M135 152c7-16 18-24 29-24 13 0 27 13 34 38" fill="#d9a85f" opacity=".2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB