From cb1af46bb4d6052b38bca73d117f699456fca5de Mon Sep 17 00:00:00 2001 From: matt-edmondson Date: Sun, 27 Sep 2026 07:25:08 +0000 Subject: [PATCH] [patch] Keep repeated items within a list when merging properties MergeArrayValues collected every list into one HashSet, so it removed duplicates inside a single list as well as across the lists being merged. Because even singleton keys go through it, CombineFrontmatter under the default Conservative strategy turned steps: [mix, wait, mix] into [mix, wait] on a document with nothing to merge. Lists are now appended in order, and an item is dropped only when an earlier list in the same merge already supplied it. Fixes #142 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KMqfUfzWj2Gmdz1EqYdnLe --- Frontmatter.Test/PropertyMergerTests.cs | 44 +++++++++++++++++++++++++ Frontmatter/PropertyMerger.cs | 18 +++++----- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/Frontmatter.Test/PropertyMergerTests.cs b/Frontmatter.Test/PropertyMergerTests.cs index 6d758e5..8a72a51 100644 --- a/Frontmatter.Test/PropertyMergerTests.cs +++ b/Frontmatter.Test/PropertyMergerTests.cs @@ -745,4 +745,48 @@ public void CombineFrontmatter_WithMaximumStrategy_MergesAllRelevantProperties() // Tags variants should be merged Assert.IsTrue(extractedFrontmatter.ContainsKey("tags"), "Result should contain key 'tags'"); } + + [TestMethod] + public void MergeSimilarProperties_SingleListWithRepeatedItems_KeepsEveryItem() + { + Dictionary frontmatter = new() + { + { "steps", new List { "mix", "wait", "mix" } }, + { "scores", new List { "1", "1", "2" } } + }; + + Dictionary result = PropertyMerger.MergeSimilarProperties(frontmatter, FrontmatterMergeStrategy.Conservative); + + CollectionAssert.AreEqual(new object[] { "mix", "wait", "mix" }, (System.Collections.ICollection)result["steps"]); + CollectionAssert.AreEqual(new object[] { "1", "1", "2" }, (System.Collections.ICollection)result["scores"]); + } + + [TestMethod] + public void MergeSimilarProperties_MergedListsRepeatWithinOneList_KeepsTheRepeatsAndDropsOnlyCrossListDuplicates() + { + Dictionary frontmatter = new() + { + { "tags", new List { "a", "a", "c" } }, + { "tag", new List { "b", "c", "b" } } + }; + + Dictionary result = PropertyMerger.MergeSimilarProperties(frontmatter, FrontmatterMergeStrategy.Conservative); + + Assert.HasCount(1, result); + CollectionAssert.AreEqual(new object[] { "a", "a", "c", "b", "b" }, (System.Collections.ICollection)result["tags"]); + } + + [TestMethod] + public void CombineFrontmatter_SingleBlockWithRepeatedListItems_RoundTripsTheLists() + { + string nl = Environment.NewLine; + string input = $"---{nl}steps: [mix, wait, mix]{nl}scores: [1, 1, 2]{nl}---{nl}Body{nl}"; + + string result = Frontmatter.CombineFrontmatter(input, FrontmatterNaming.AsIs, FrontmatterOrder.AsIs, FrontmatterMergeStrategy.Conservative); + Dictionary? frontmatter = Frontmatter.ExtractFrontmatter(result); + + Assert.IsNotNull(frontmatter); + CollectionAssert.AreEqual(new object[] { "mix", "wait", "mix" }, (System.Collections.ICollection)frontmatter["steps"]); + CollectionAssert.AreEqual(new object[] { "1", "1", "2" }, (System.Collections.ICollection)frontmatter["scores"]); + } } diff --git a/Frontmatter/PropertyMerger.cs b/Frontmatter/PropertyMerger.cs index 9eac550..70ab447 100644 --- a/Frontmatter/PropertyMerger.cs +++ b/Frontmatter/PropertyMerger.cs @@ -172,18 +172,20 @@ private static void MergeArrayValues( string canonicalKey, List originalKeys) { - HashSet mergedList = []; + // Lists are appended in order. An item is dropped only when an earlier list being merged already + // supplied it, never because it repeats within its own list: a list such as recipe steps or + // scores can legitimately hold the same value more than once. + List mergedList = []; + HashSet fromEarlierLists = []; foreach (string key in originalKeys) { - object value = source[key]; - if (value is IList list) + if (source[key] is not IList items) { - mergedList.UnionWith(list); - } - else if (value is object[] array) - { - mergedList.UnionWith(array); + continue; } + + mergedList.AddRange(items.Where(item => !fromEarlierLists.Contains(item))); + fromEarlierLists.UnionWith(items); } target[canonicalKey] = mergedList.ToArray();