From feed4549bfc6312443076d40a13c2d712483eee5 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sat, 29 Aug 2026 13:23:52 +0000 Subject: [PATCH] Order relationship review by first appearance --- PlotLine.Tests/Program.cs | 9 +++++++++ .../StoryIntelligenceRelationshipImportService.cs | 11 ++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index d185a4c..282aa17 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -54,6 +54,7 @@ var tests = new (string Name, Action Test)[] ("Relationship events preserve chronological story order", RelationshipEventsPreserveChronologicalStoryOrder), ("Relationship progression keeps chronological state", RelationshipProgressionKeepsChronologicalState), ("Relationship observations do not create event spam", RelationshipObservationsDoNotCreateEventSpam), + ("Relationship review orders by first story appearance", RelationshipReviewOrdersByFirstStoryAppearance), ("Knowledge signals map to existing knowledge states", KnowledgeSignalsMapToExistingStates), ("Knowledge recipient resolves narrator through POV", KnowledgeRecipientResolvesNarratorThroughPov), ("Knowledge identity reveal preserves subjective timing", KnowledgeIdentityRevealPreservesSubjectiveTiming), @@ -2200,6 +2201,14 @@ static void RelationshipObservationsDoNotCreateEventSpam() Assert(!IsMeaningfulRelationshipEvent("trust", "Nora and Imani exchange gifts after years of friendship."), "The non-event rule should hold for unrelated fixture names too."); } +static void RelationshipReviewOrdersByFirstStoryAppearance() +{ + var service = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Services/StoryIntelligenceRelationshipImportService.cs")); + Assert(service.Contains(".OrderBy(CandidateFirstStoryOrder)", StringComparison.Ordinal), "Relationship review candidates should be ordered by first story appearance."); + Assert(service.Contains("private static (decimal ChapterNumber, decimal SceneNumber, int SortOrder, int SceneID) CandidateFirstStoryOrder", StringComparison.Ordinal), "Relationship review should calculate first appearance from chapter/scene order."); + Assert(!service.Contains(".OrderByDescending(candidate => candidate.Appearances.Select(appearance => appearance.SceneID).Distinct().Count())", StringComparison.Ordinal), "Relationship review should not prioritise frequent relationships ahead of earlier first appearances."); +} + static void KnowledgeSignalsMapToExistingStates() { var states = new List diff --git a/PlotLine/Services/StoryIntelligenceRelationshipImportService.cs b/PlotLine/Services/StoryIntelligenceRelationshipImportService.cs index d974234..b90c225 100644 --- a/PlotLine/Services/StoryIntelligenceRelationshipImportService.cs +++ b/PlotLine/Services/StoryIntelligenceRelationshipImportService.cs @@ -306,7 +306,7 @@ public sealed class StoryIntelligenceRelationshipImportService( var candidates = groups.Values .Where(candidate => candidate.Appearances.Any(appearance => !appearance.AlreadyLinked)) - .OrderByDescending(candidate => candidate.Appearances.Select(appearance => appearance.SceneID).Distinct().Count()) + .OrderBy(CandidateFirstStoryOrder) .ThenBy(candidate => candidate.CharacterAName) .ThenBy(candidate => candidate.CharacterBName) .ToList(); @@ -588,6 +588,15 @@ public sealed class StoryIntelligenceRelationshipImportService( private static (decimal ChapterNumber, decimal SceneNumber, int SortOrder, int SceneID) StoryOrder(RelationshipAppearanceImport appearance) => (appearance.ChapterNumber, appearance.SceneNumber, appearance.SortOrder, appearance.SceneID); + private static (decimal ChapterNumber, decimal SceneNumber, int SortOrder, int SceneID) CandidateFirstStoryOrder(RelationshipCandidate candidate) + => candidate.Appearances + .Select(StoryOrder) + .OrderBy(order => order.ChapterNumber) + .ThenBy(order => order.SceneNumber) + .ThenBy(order => order.SortOrder) + .ThenBy(order => order.SceneID) + .FirstOrDefault((decimal.MaxValue, decimal.MaxValue, int.MaxValue, int.MaxValue)); + private static string RelationshipStateAtScene( string initialState, IEnumerable<(int SceneID, decimal ChapterNumber, decimal SceneNumber, int SortOrder, string StateName)> events,