Skip to content

Commit 9b3b9e0

Browse files
konardclaude
andcommitted
Merge branch 'main' into issue-65-5d06345e60dc
Resolved conflict in rust/src/changes_simplifier.rs: - Kept new remove_duplicate_before_states function from main (Issue #26 fix) - Moved inline tests to separate test file per reviewer feedback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2 parents e1b1ea9 + 63c9274 commit 9b3b9e0

File tree

8 files changed

+382
-0
lines changed

8 files changed

+382
-0
lines changed

csharp/Foundation.Data.Doublets.Cli.Tests/ChangesSimplifier.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,77 @@ public void SimplifyChanges_SpecificExample_KeepsUnchangedStates()
327327
AssertChangeSetEqual(expectedSimplifiedChanges, simplifiedChanges);
328328
}
329329

330+
[Fact]
331+
public void SimplifyChanges_Issue26_UpdateOperationSimplification()
332+
{
333+
// Arrange - This represents the scenario described in GitHub issue #26
334+
// Where links (1: 1 2) and (2: 2 1) are being updated to swap source and target
335+
// The issue was that intermediate steps were being shown instead of the final transformation
336+
var changes = new List<(Link<uint> Before, Link<uint> After)>
337+
{
338+
// Step 1: Link (1: 1 2) is first deleted (becomes null/empty)
339+
(new Link<uint>(index: 1, source: 1, target: 2), new Link<uint>(index: 0, source: 0, target: 0)),
340+
341+
// Step 2: New link (1: 2 1) is created (swapped source and target)
342+
(new Link<uint>(index: 0, source: 0, target: 0), new Link<uint>(index: 1, source: 2, target: 1)),
343+
344+
// Step 3: Link (2: 2 1) is directly updated to (2: 1 2)
345+
(new Link<uint>(index: 2, source: 2, target: 1), new Link<uint>(index: 2, source: 1, target: 2)),
346+
};
347+
348+
// Expected - The simplification should show only the initial-to-final transformations
349+
var expectedSimplifiedChanges = new List<(Link<uint> Before, Link<uint> After)>
350+
{
351+
(new Link<uint>(index: 1, source: 1, target: 2), new Link<uint>(index: 1, source: 2, target: 1)),
352+
(new Link<uint>(index: 2, source: 2, target: 1), new Link<uint>(index: 2, source: 1, target: 2)),
353+
};
354+
355+
// Act
356+
var simplifiedChanges = SimplifyChanges(changes).ToList();
357+
358+
// Assert
359+
AssertChangeSetEqual(expectedSimplifiedChanges, simplifiedChanges);
360+
}
361+
362+
[Fact]
363+
public void SimplifyChanges_Issue26_AlternativeScenario_NoSimplificationOccurs()
364+
{
365+
// Arrange - This tests a different scenario that might represent the actual issue
366+
// Maybe the problem is that the changes are NOT being chained correctly
367+
// Let's simulate what might happen if the simplifier doesn't work correctly
368+
var changes = new List<(Link<uint> Before, Link<uint> After)>
369+
{
370+
// Let's say we get these individual changes that don't form a proper chain
371+
(new Link<uint>(index: 1, source: 1, target: 2), new Link<uint>(index: 0, source: 0, target: 0)), // delete
372+
(new Link<uint>(index: 1, source: 1, target: 2), new Link<uint>(index: 1, source: 2, target: 1)), // direct update (this might be what's reported)
373+
(new Link<uint>(index: 2, source: 2, target: 1), new Link<uint>(index: 2, source: 1, target: 2)), // direct update
374+
};
375+
376+
// Act
377+
var simplifiedChanges = SimplifyChanges(changes).ToList();
378+
379+
// Debug output
380+
Console.WriteLine("=== Debug: Alternative Scenario ===");
381+
Console.WriteLine("Input changes:");
382+
for (int i = 0; i < changes.Count; i++)
383+
{
384+
var (b, a) = changes[i];
385+
Console.WriteLine($" {i + 1}. ({b.Index}: {b.Source} {b.Target}) -> ({a.Index}: {a.Source} {a.Target})");
386+
}
387+
388+
Console.WriteLine("Actual simplified changes:");
389+
for (int i = 0; i < simplifiedChanges.Count; i++)
390+
{
391+
var (b, a) = simplifiedChanges[i];
392+
Console.WriteLine($" {i + 1}. ({b.Index}: {b.Source} {b.Target}) -> ({a.Index}: {a.Source} {a.Target})");
393+
}
394+
Console.WriteLine($"Count: {simplifiedChanges.Count}");
395+
Console.WriteLine("=== End Debug ===");
396+
397+
// The issue might be that we get 3 changes instead of 2
398+
// If the simplifier doesn't work, we'd see all 3 changes
399+
}
400+
330401
private static void AssertChangeSetEqual(
331402
List<(Link<uint> Before, Link<uint> After)> expectedSimplifiedChanges,
332403
List<(Link<uint> Before, Link<uint> After)> simplifiedChanges

csharp/Foundation.Data.Doublets.Cli/ChangesSimplifier.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ public static class ChangesSimplifier
2727
return Enumerable.Empty<(Link<uint>, Link<uint>)>();
2828
}
2929

30+
// **FIX for Issue #26**: Remove duplicate before states by keeping the last occurrence
31+
// This handles cases where the same link is reported with multiple different transformations
32+
changesList = RemoveDuplicateBeforeStates(changesList);
33+
3034
// First, handle unchanged states directly
3135
var unchangedStates = new List<(Link<uint> Before, Link<uint> After)>();
3236
var changedStates = new List<(Link<uint> Before, Link<uint> After)>();
@@ -124,6 +128,61 @@ public static class ChangesSimplifier
124128
.ThenBy(r => r.After.Target);
125129
}
126130

131+
/// <summary>
132+
/// Removes problematic duplicate before states that lead to simplification issues.
133+
/// This fixes Issue #26 where multiple transformations from the same before state
134+
/// to conflicting after states (including null states) would cause the simplifier to fail.
135+
///
136+
/// The key insight: If we have multiple transitions from the same before state,
137+
/// and one of them is to a "null" state (0: 0 0), we should prefer the non-null transition
138+
/// as it represents the actual final transformation.
139+
/// </summary>
140+
/// <param name="changes">The list of changes that may contain problematic duplicate before states</param>
141+
/// <returns>A list with problematic duplicates resolved</returns>
142+
private static List<(Link<uint> Before, Link<uint> After)> RemoveDuplicateBeforeStates(
143+
List<(Link<uint> Before, Link<uint> After)> changes)
144+
{
145+
// Group changes by their before state
146+
var groupedChanges = changes.GroupBy(c => c.Before, LinkEqualityComparer.Instance);
147+
148+
var result = new List<(Link<uint> Before, Link<uint> After)>();
149+
150+
foreach (var group in groupedChanges)
151+
{
152+
var changesForThisBefore = group.ToList();
153+
154+
if (changesForThisBefore.Count == 1)
155+
{
156+
// No duplicates, keep as is
157+
result.AddRange(changesForThisBefore);
158+
}
159+
else
160+
{
161+
// Multiple changes from the same before state
162+
// Check if any of them is to a null state (0: 0 0)
163+
var nullTransition = changesForThisBefore.FirstOrDefault(c =>
164+
c.After.Index == 0 && c.After.Source == 0 && c.After.Target == 0);
165+
var nonNullTransitions = changesForThisBefore.Where(c =>
166+
!(c.After.Index == 0 && c.After.Source == 0 && c.After.Target == 0)).ToList();
167+
168+
if (nullTransition != default && nonNullTransitions.Count > 0)
169+
{
170+
// Issue #26 scenario: We have both null and non-null transitions
171+
// Prefer the non-null transitions as they represent the actual final states
172+
result.AddRange(nonNullTransitions);
173+
}
174+
else
175+
{
176+
// No null transitions involved, this is a legitimate multiple-branch scenario
177+
// Keep all transitions
178+
result.AddRange(changesForThisBefore);
179+
}
180+
}
181+
}
182+
183+
return result;
184+
}
185+
127186
/// <summary>
128187
/// An equality comparer for Link<uint> that checks Index/Source/Target.
129188
/// </summary>

csharp/Foundation.Data.Doublets.Cli/Program.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,27 @@
131131

132132
if (changes && changesList.Any())
133133
{
134+
// Debug: Print raw changes before simplification (if trace is enabled)
135+
if (trace)
136+
{
137+
Console.WriteLine("[DEBUG] Raw changes before simplification:");
138+
for (int i = 0; i < changesList.Count; i++)
139+
{
140+
var (beforeLink, afterLink) = changesList[i];
141+
Console.WriteLine($"[DEBUG] {i + 1}. ({beforeLink.Index}: {beforeLink.Source} {beforeLink.Target}) -> ({afterLink.Index}: {afterLink.Source} {afterLink.Target})");
142+
}
143+
Console.WriteLine($"[DEBUG] Total raw changes: {changesList.Count}");
144+
}
145+
134146
// Simplify the collected changes
135147
var simplifiedChanges = SimplifyChanges(changesList);
136148

149+
// Debug: Print simplified changes count (if trace is enabled)
150+
if (trace)
151+
{
152+
Console.WriteLine($"[DEBUG] Simplified changes count: {simplifiedChanges.Count()}");
153+
}
154+
137155
// Print the simplified changes
138156
foreach (var (linkBefore, linkAfter) in simplifiedChanges)
139157
{

examples/debug_changes.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using Platform.Data.Doublets;
2+
using Foundation.Data.Doublets.Cli;
3+
using static Foundation.Data.Doublets.Cli.ChangesSimplifier;
4+
5+
// Test the problematic scenario from the issue
6+
var changes = new List<(Link<uint> Before, Link<uint> After)>
7+
{
8+
// This simulates what might be happening in the update operation
9+
// Let's say we have links being swapped: (1:1 2) -> (1:2 1) and (2:2 1) -> (2:1 2)
10+
11+
// From the issue it looks like these might be the intermediate steps:
12+
(new Link<uint>(index: 1, source: 1, target: 2), new Link<uint>(index: 1, source: 2, target: 1)),
13+
(new Link<uint>(index: 2, source: 2, target: 1), new Link<uint>(index: 2, source: 1, target: 2)),
14+
};
15+
16+
Console.WriteLine("Original changes:");
17+
foreach (var (before, after) in changes)
18+
{
19+
Console.WriteLine($"({before.Index}: {before.Source} {before.Target}) -> ({after.Index}: {after.Source} {after.Target})");
20+
}
21+
22+
Console.WriteLine("\nSimplified changes:");
23+
var simplified = SimplifyChanges(changes).ToList();
24+
foreach (var (before, after) in simplified)
25+
{
26+
Console.WriteLine($"({before.Index}: {before.Source} {before.Target}) -> ({after.Index}: {after.Source} {after.Target})");
27+
}

examples/test1_output.txt

Whitespace-only changes.

examples/test_issue_scenario.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Platform.Data.Doublets;
5+
using Foundation.Data.Doublets.Cli;
6+
using static Foundation.Data.Doublets.Cli.ChangesSimplifier;
7+
8+
// Simulate the exact scenario from the issue
9+
Console.WriteLine("=== Testing Issue #26 Scenario ===");
10+
11+
// This simulates what might be happening based on the issue output:
12+
// ((1: 1 2)) () - Link (1: 1 2) is deleted
13+
// ((1: 1 2)) ((1: 2 1)) - Link (1: 1 2) becomes (1: 2 1)
14+
// ((2: 2 1)) ((2: 1 2)) - Link (2: 2 1) becomes (2: 1 2)
15+
16+
var changes = new List<(Link<uint> Before, Link<uint> After)>
17+
{
18+
// First transformation: (1: 1 2) -> () (deletion)
19+
(new Link<uint>(index: 1, source: 1, target: 2), new Link<uint>(index: 0, source: 0, target: 0)),
20+
21+
// Second transformation: () -> (1: 2 1) (creation)
22+
(new Link<uint>(index: 0, source: 0, target: 0), new Link<uint>(index: 1, source: 2, target: 1)),
23+
24+
// Third transformation: (2: 2 1) -> (2: 1 2) (direct update)
25+
(new Link<uint>(index: 2, source: 2, target: 1), new Link<uint>(index: 2, source: 1, target: 2)),
26+
};
27+
28+
Console.WriteLine("Original changes (as they might occur in the system):");
29+
for (int i = 0; i < changes.Count; i++)
30+
{
31+
var (before, after) = changes[i];
32+
Console.WriteLine($"{i + 1}. ({before.Index}: {before.Source} {before.Target}) -> ({after.Index}: {after.Source} {after.Target})");
33+
}
34+
35+
Console.WriteLine("\nSimplified changes (what should be shown):");
36+
var simplified = SimplifyChanges(changes).ToList();
37+
for (int i = 0; i < simplified.Count; i++)
38+
{
39+
var (before, after) = simplified[i];
40+
Console.WriteLine($"{i + 1}. ({before.Index}: {before.Source} {before.Target}) -> ({after.Index}: {after.Source} {after.Target})");
41+
}
42+
43+
Console.WriteLine("\n=== Expected vs Actual ===");
44+
Console.WriteLine("Expected: Only the final transformations should be shown");
45+
Console.WriteLine("- (1: 1 2) -> (1: 2 1)");
46+
Console.WriteLine("- (2: 2 1) -> (2: 1 2)");
47+
48+
Console.WriteLine($"\nActual count: {simplified.Count}");
49+
Console.WriteLine("If this count is more than 2, then the issue is reproduced.");

rust/src/changes_simplifier.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ pub fn simplify_changes(changes: Vec<(Link, Link)>) -> Vec<(Link, Link)> {
1616
return vec![];
1717
}
1818

19+
// **FIX for Issue #26**: Remove duplicate before states by keeping the non-null transitions
20+
// This handles cases where the same link is reported with multiple different transformations
21+
let changes = remove_duplicate_before_states(changes);
22+
1923
// First, handle unchanged states directly
2024
let mut unchanged_states = Vec::new();
2125
let mut changed_states = Vec::new();
@@ -105,3 +109,51 @@ pub fn simplify_changes(changes: Vec<(Link, Link)>) -> Vec<(Link, Link)> {
105109

106110
results
107111
}
112+
113+
/// Removes problematic duplicate before states that lead to simplification issues.
114+
/// This fixes Issue #26 where multiple transformations from the same before state
115+
/// to conflicting after states (including null states) would cause the simplifier to fail.
116+
///
117+
/// The key insight: If we have multiple transitions from the same before state,
118+
/// and one of them is to a "null" state (0: 0 0), we should prefer the non-null transition
119+
/// as it represents the actual final transformation.
120+
fn remove_duplicate_before_states(changes: Vec<(Link, Link)>) -> Vec<(Link, Link)> {
121+
// Group changes by their before state
122+
let mut grouped: HashMap<Link, Vec<(Link, Link)>> = HashMap::new();
123+
for change in changes {
124+
grouped.entry(change.0).or_default().push(change);
125+
}
126+
127+
let mut result = Vec::new();
128+
129+
for (_before, changes_for_this_before) in grouped {
130+
if changes_for_this_before.len() == 1 {
131+
// No duplicates, keep as is
132+
result.extend(changes_for_this_before);
133+
} else {
134+
// Multiple changes from the same before state
135+
// Check if any of them is to a null state (0: 0 0)
136+
let null_link = Link::new(0, 0, 0);
137+
let has_null_transition = changes_for_this_before
138+
.iter()
139+
.any(|(_, after)| *after == null_link);
140+
let non_null_transitions: Vec<_> = changes_for_this_before
141+
.iter()
142+
.filter(|(_, after)| *after != null_link)
143+
.cloned()
144+
.collect();
145+
146+
if has_null_transition && !non_null_transitions.is_empty() {
147+
// Issue #26 scenario: We have both null and non-null transitions
148+
// Prefer the non-null transitions as they represent the actual final states
149+
result.extend(non_null_transitions);
150+
} else {
151+
// No null transitions involved, this is a legitimate multiple-branch scenario
152+
// Keep all transitions
153+
result.extend(changes_for_this_before);
154+
}
155+
}
156+
}
157+
158+
result
159+
}

0 commit comments

Comments
 (0)