From b2b7fbc735cc63f49ea02aa86de88a4f8c91e57c Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sun, 26 Jul 2026 15:38:31 +0000 Subject: [PATCH] Strengthen pronoun presentation evidence --- PlotLine.Tests/Program.cs | 8 ++++++-- .../StoryIntelligenceIllustrationCompatibility.cs | 7 +++++-- .../css/story-intelligence-experience-prototype.css | 2 +- .../wwwroot/js/story-intelligence-experience-prototype.js | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 46a6dca..eaf396f 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -1090,6 +1090,8 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules() new StoryMemoryCharacterAttribute { AttributeType = "AgeBand", NormalisedValue = StoryIntelligenceIllustrationCompatibility.AgeBands.Child, Confidence = 0.98m, IsExplicit = true, EvidenceSummary = "Deterministic evidence inference" }, new StoryMemoryCharacterAttribute { AttributeType = "AgeBand", NormalisedValue = StoryIntelligenceIllustrationCompatibility.AgeBands.YoungAdult, Confidence = 0.90m, IsExplicit = true, EvidenceSummary = "She may be eighteen now, but she is still the baby of the group." } ]); + var alisonPronoun = StoryIntelligenceIllustrationCompatibility.NormalizePresentation("She was a nurse at Selly Oak Hospital."); + var alisonName = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Alison", ["Alison"], []); var gregFromRosieAction = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Greg", ["Greg offers to take Rosie to hospital."], []); var childCandidate = Metadata("char-child", "Child", "Feminine"); var darkRedCandidate = Metadata("char-red-dark", "OlderTeen", "Feminine", hairColour: "Red", skinTone: "Dark"); @@ -1097,6 +1099,8 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules() Assert(bethFromLeakyEvidence.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"Name/title guard should repair leaked Beth presentation, got {bethFromLeakyEvidence.Presentation}."); Assert(mrsWithTeenContext.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, $"Adult-role titles must not be overridden by primary age context, got {mrsWithTeenContext.AgeBand}."); Assert(rosieBirthday.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.YoungAdult, $"Rosie's explicit eighteen-year-old evidence must beat weak child leakage, got {rosieBirthday.AgeBand}."); + Assert(alisonPronoun == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"Direct she/her pronoun evidence should infer Feminine, got {alisonPronoun}."); + Assert(alisonName.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"Common feminine name Alison should infer Feminine, got {alisonName.Presentation}."); Assert(gregFromRosieAction.Presentation != StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, "Greg must not inherit feminine presentation from action text mentioning Rosie."); Assert(Rejected(mum, childCandidate).Any(reason => reason.Contains("age hard mismatch", StringComparison.OrdinalIgnoreCase)), "Parent role should reject child portraits."); Assert(Rejected(redHairedMaggie, darkRedCandidate).Any(reason => reason.Contains("skin tone hard mismatch", StringComparison.OrdinalIgnoreCase)), "Red-haired light-skin evidence must reject dark-skin generated portraits."); @@ -1183,8 +1187,8 @@ static void Phase21RSceneBrowserAndPanelsAreReplayFriendly() Assert(css.Contains("--story-side-body-font-size: 0.72rem;", StringComparison.Ordinal), "Left and right side panels should share body text sizing."); Assert(!view.Contains("data-assets", StringComparison.Ordinal), "Asset visualisation lane should not be rendered on the main stage."); Assert(script.Contains("Assets are recorded in insights/diagnostics but not rendered as stage nodes.", StringComparison.Ordinal), "Asset data should remain available without drawing asset stage nodes."); - Assert(script.Contains("width * 0.70 - size / 2", StringComparison.Ordinal), "Location should use the former asset space once asset nodes are removed."); - Assert(css.Contains(".story-exp-zone--location .story-exp-zone-label {\n left: 67%;", StringComparison.Ordinal), "Location lane label should follow the rebalanced stage."); + Assert(script.Contains("width * 0.76 - size / 2", StringComparison.Ordinal), "Location should use the former asset space once asset nodes are removed."); + Assert(css.Contains(".story-exp-zone--location .story-exp-zone-label {\n left: 73%;", StringComparison.Ordinal), "Location lane label should follow the rebalanced stage."); Assert(!css.Contains("grid-template-rows: minmax(218px, 1.35fr) minmax(185px, 1.08fr) auto", StringComparison.Ordinal), "Right rail must not use fixed-height panel tracks."); Assert(!css.Contains("grid-template-rows: auto auto minmax(170px, 1fr) minmax(96px, 0.56fr)", StringComparison.Ordinal), "Left rail must not use fixed-height panel tracks."); Assert(script.Contains("shouldUseCharacterPortrait", StringComparison.Ordinal), "Characters should use evidence cards until portrait evidence is strong enough."); diff --git a/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs b/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs index e459f92..f93f227 100644 --- a/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs +++ b/PlotLine/Services/StoryIntelligenceIllustrationCompatibility.cs @@ -222,6 +222,9 @@ public static class StoryIntelligenceIllustrationCompatibility public static string NormalizePresentation(string? value) { var text = (value ?? string.Empty).ToLowerInvariant(); + var padded = $" {text} "; + if (ContainsAny(padded, " she ", " her ", " hers ", " herself ")) return Presentations.Feminine; + if (ContainsAny(padded, " he ", " him ", " his ", " himself ")) return Presentations.Masculine; if (ContainsAny(text, "feminine", "female", "woman", "girl", "mother", "mum", "mrs", "miss", "ms", "aunt", "sister", "daughter", "wife", "grandmother")) return Presentations.Feminine; if (ContainsAny(text, "masculine", "male", "man", "boy", "father", "dad", "mr", "uncle", "brother", "son", "husband", "grandfather")) return Presentations.Masculine; if (text.Contains("androgynous", StringComparison.OrdinalIgnoreCase)) return Presentations.Androgynous; @@ -512,7 +515,7 @@ public static class StoryIntelligenceIllustrationCompatibility private static EvidenceSignal NamePresentation(string text) { - if (ContainsAny(text, " beth ", " maggie ", " rosie ", " grace ", " zoe ", " rebecca ", " annie ", " mary ", " sharon ", " catherine ", " susan ", " helen ", " elen ")) return new(Presentations.Feminine, 0.82m); + if (ContainsAny(text, " alison ", " beth ", " maggie ", " rosie ", " grace ", " zoe ", " rebecca ", " annie ", " mary ", " sharon ", " catherine ", " susan ", " helen ", " elen ")) return new(Presentations.Feminine, 0.82m); if (ContainsAny(text, " adam ", " colin ", " david ", " kevin ", " simon ", " graham ", " gareth ", " john ", " michael ", " peter ", " richard ", " rick ", " george ")) return new(Presentations.Masculine, 0.82m); return EvidenceSignal.Unknown; } @@ -555,7 +558,7 @@ public static class StoryIntelligenceIllustrationCompatibility } if (StartsWithAny(clean, "mrs", "miss", "ms", "mother", "mum", "mom", "aunt", "sister", "daughter", "girl", "woman", "female") - || clean is "beth" or "maggie" or "grace" or "rosie" or "rebecca" or "annie" or "mary" or "susan" or "helen" or "elen" or "zoe" or "sharon" or "catherine") + || clean is "alison" or "beth" or "maggie" or "grace" or "rosie" or "rebecca" or "annie" or "mary" or "susan" or "helen" or "elen" or "zoe" or "sharon" or "catherine") { return new(Presentations.Feminine, 0.98m); } diff --git a/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css index b11f061..4b7fa1e 100644 --- a/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css +++ b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css @@ -777,7 +777,7 @@ input:focus-visible { } .story-exp-zone--location .story-exp-zone-label { - left: 67%; + left: 73%; top: 22px; } diff --git a/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js b/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js index 243d44a..465ef8f 100644 --- a/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js +++ b/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js @@ -907,7 +907,7 @@ const height = Math.max(500, bounds?.height || 560); const size = Math.min(220, Math.max(176, width * 0.18)); return { - x: Math.round(width * 0.70 - size / 2), + x: Math.round(width * 0.76 - size / 2), y: Math.round(height * 0.46 - size / 2), size };