Skip to content

Improve list diffing with sequence alignment to avoid cascading changes - #206

Merged
stefankoegl merged 3 commits into
masterfrom
claude/inspiring-knuth-wrbtbp
Oct 9, 2026
Merged

stefankoegl merged 3 commits into
masterfrom
claude/inspiring-knuth-wrbtbp

Conversation

@stefankoegl

@stefankoegl stefankoegl commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR improves the JSON patch generation algorithm for lists by implementing intelligent sequence alignment. Instead of comparing list items by index position, the algorithm now identifies which items have actually moved or changed, preventing cascading modifications when items are inserted or removed.

Key Changes

  • Added sequence alignment logic (_changed_blocks function): Uses difflib.SequenceMatcher to intelligently align source and destination lists, identifying blocks of changes rather than comparing by index alone
  • Introduced item key hashing (_item_key method): Creates hashable keys for complex items (dicts, lists) to enable proper item matching across list transformations, with keys based on content rather than identity
  • Refactored list comparison (_compare_lists method): Completely rewrote to use the new alignment algorithm, which maps items by their content and only reports actual changes
  • Extracted item comparison logic (_compare_items method): Separated the recursive comparison logic into its own method for better code organization
  • Added fallback to positional comparison: When sequence alignment would result in more changes than simple index-based comparison, the algorithm falls back to positional comparison to avoid over-reporting changes
  • Enabled previously disabled test: test_should_just_add_new_item_not_rebuild_all_list is now enabled and passing, demonstrating the fix for issue Fix optimizing list insertion/deletion diffs #83

Notable Implementation Details

  • The alignment algorithm first checks for common prefixes and suffixes to minimize the comparison window
  • _changed_items helper function evaluates the cost of different alignment strategies to choose the optimal one
  • Item keys are computed recursively for nested structures, ensuring proper matching even for complex nested objects
  • The solution handles edge cases like long sequences with repeated values and maintains backward compatibility with existing behavior

Test Coverage

Added comprehensive test cases covering:

  • Inserting objects into lists without modifying existing items
  • Removing items from lists
  • Combined insert and remove operations
  • Long lists with repeated values
  • Alignment correctness when positional comparison is better
  • Key order independence in object matching

https://claude.ai/code/session_01R62pG28eF1nRucQY4KsxoJ

fixes #83

make_patch compared list items by index, so inserting or removing an
item made every item after it look changed. Move detection undid some of
that for scalars, but lists of objects or arrays were rebuilt field by
field: inserting one object at the front of a list of 2000 gave 4003
operations instead of one 'add'.

Lists are now aligned before their items are compared. A common prefix
and suffix are skipped, and difflib.SequenceMatcher aligns the rest.
SequenceMatcher ignores items that occur often in long lists, so its
alignment is only used if it changes fewer items than comparing them by
index. Items are matched by a key that follows the existing notion of
equality: leaves are compared by their JSON dump, so 1 and true stay
different, and the key order of objects does not matter.

This re-enables test_should_just_add_new_item_not_rebuild_all_list,
which was disabled when the DiffBuilder was introduced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R62pG28eF1nRucQY4KsxoJ

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Alignment can generate incorrect patches for JSON-distinct containers, and its runtime is unbounded quadratic.

2 open findings
What changed in this PR

Improves list diff generation by aligning unchanged items to avoid cascading patch operations.

Changes:

  • Adds content-based item keys and sequence alignment.
  • Refactors recursive item comparison and expands list-diff tests.
File Description
jsonpatch.py Implements aligned list diffing.
tests.py Adds insertion, removal, repetition, and ordering cases.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread jsonpatch.py Outdated
Comment thread jsonpatch.py Outdated
Move detection stored values by (value, type(value)) and compared
unhashable ones with ==, so [1] and [true] were considered equal and one
was moved where the other belongs. Aligning lists made this reachable
from more inputs, e.g. [[1], [2], [3]] -> [[2], [3], [true]]. Values are
now stored by the same JSON-aware key that aligns list items (#180).
Values that dumps cannot serialize get a key equal only to themselves,
so they can still be added, removed and moved as before.

SequenceMatcher compares each item with the equal items of the other
list, which took 22 seconds for a list of 100,000 repeated numbers whose
first and last items were replaced. Aligning is now skipped if that
takes more comparisons than comparing by index would take steps, which
is quadratic in the number of items changed by index, as the diff turns
them into moves, with a floor for lists that differ in few items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R62pG28eF1nRucQY4KsxoJ

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The core diff and move-detection rewrite has broad behavioral and performance implications requiring final human review.

0 open findings

2 resolved since last review

🧠 Review effort: Balanced

Master now matches up list items itself (_differing_runs, #78), which
replaces the alignment of this branch: it also estimates operations
recursively and matches up all items of shorter lists. What this branch
adds on top of it:

- Move detection keys values like _differing_runs does, so that values
  that are equal only in Python (e.g. [1] and [true]) are no longer
  moved where the other belongs. Values that cannot be serialized are
  only moved where they are added unchanged.
- For long lists, matching up is skipped if SequenceMatcher would take
  more comparisons than max(_EXACT_MATCH_LIMIT, operations by position
  squared). It took 22 s for 100,000 repeated numbers whose first and
  last items were replaced, and takes 0.8 s now.
- The test from #83 is enabled, and tests of this branch that master's
  tests for #78 cover are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R62pG28eF1nRucQY4KsxoJ
@stefankoegl
stefankoegl merged commit 154627e into master Oct 9, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix optimizing list insertion/deletion diffs

3 participants