Class: CoPlan::Plans::TransformRange
- Inherits:
-
Object
- Object
- CoPlan::Plans::TransformRange
- Defined in:
- app/services/coplan/plans/transform_range.rb
Overview
Operational Transform engine for character-range transformations.
INVARIANT: Every PlanVersion’s operations_json entries MUST contain positional metadata (resolved_range + new_range/delta, or replacements). All content edits flow through Plans::ApplyOperations which stores this data. There are no legacy or non-positional edit paths.
If an operation lacks positional metadata, transform_through_versions raises Conflict rather than silently skipping it.
Defined Under Namespace
Classes: Conflict
Class Method Summary collapse
-
.transform(range, edit_data) ⇒ Object
Transform a range [s, e] through an edit that replaced [s2, e2] with text of length new_length.
-
.transform_through_versions(range, versions) ⇒ Object
Transform a range through a sequence of edits from a PlanVersion.
Class Method Details
.transform(range, edit_data) ⇒ Object
Transform a range [s, e] through an edit that replaced [s2, e2] with text of length new_length. Returns the transformed [s, e] or raises Conflict if ranges overlap.
edit_data is a hash with:
resolved_range: [s2, e2]
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'app/services/coplan/plans/transform_range.rb', line 22 def self.transform(range, edit_data) s, e = range edit_data = edit_data.transform_keys(&:to_s) s2, e2 = edit_data["resolved_range"] # Calculate delta from the edit if edit_data.key?("new_range") new_s2, new_e2 = edit_data["new_range"] delta = (new_e2 - new_s2) - (e2 - s2) elsif edit_data.key?("delta") delta = edit_data["delta"].to_i else raise ArgumentError, "edit_data must contain 'new_range' or 'delta'" end # Zero-width insert point: special handling if s == e # Insert point: shift if edit is strictly before if e2 <= s return [s + delta, e + delta] elsif s2 > s return [s, e] else raise Conflict, "Edit overlaps with insert point" end end # Case 1: Edit is entirely before our range (e2 <= s) if e2 <= s return [s + delta, e + delta] end # Case 2: Edit is entirely after our range (s2 >= e) if s2 >= e return [s, e] end # Case 3: Overlap — conflict raise Conflict, "Ranges overlap: [#{s}, #{e}] conflicts with edit at [#{s2}, #{e2}]" end |
.transform_through_versions(range, versions) ⇒ Object
Transform a range through a sequence of edits from a PlanVersion. Each version has operations_json with resolved position data.
versions: array of PlanVersion records (or hashes with operations_json),
ordered by revision ascending
Returns the transformed range or raises Conflict.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'app/services/coplan/plans/transform_range.rb', line 70 def self.transform_through_versions(range, versions) current_range = range.dup versions.each do |version| ops = version.is_a?(Hash) ? version[:operations_json] || version["operations_json"] : version.operations_json next if ops.blank? ops.each do |op_data| op_data = op_data.transform_keys(&:to_s) if op_data.key?("replacements") # replace_all: multiple ranges, each shifts independently # Process in reverse order (highest position first) since each was # already adjusted for previous replacements during application op_data["replacements"].sort_by { |r| -r["resolved_range"][0] }.each do |replacement| current_range = transform(current_range, replacement) end elsif op_data.key?("resolved_range") current_range = transform(current_range, op_data) else raise Conflict, "Operation lacks positional metadata (resolved_range or replacements)" end end end current_range end |