Class: Ecoportal::API::GraphQL::Diff::CrossObjectDiff

Inherits:
Object
  • Object
show all
Defined in:
lib/ecoportal/api/graphql/diff/cross_object_diff.rb

Overview

Cross-object diff: two templates/pages that DO NOT share Mongo ids (UAT<->PROD, page<->template). Because nothing has the same id as its counterpart, we cannot match by id (that is VersionDiff). Instead we PAIR the fields as an equivalence problem (Pairing::Engine — genome + type + label + options, human-assisted, ledger-backed), build an id-correspondence map from the accepted pairs, then emit the SAME Change output against that map so the existing CommandSynthesizer / Deploy layer can consume it unchanged.

This deliberately operates at the FIELD level — the load-bearing simplifying principle (domain ref §6): sections/stages are scaffolding, customer data lives in the data-fields, so a cross-object diff pairs FIELDS precisely and treats structure as context. Structure-level (stage/section) reconciliation across id-spaces is not attempted here (no reliable pairing signal for scaffolding) — it is left to the human review the engine already routes to.

engine = Pairing::Engine.new(ledger: ledger) # optional diff = CrossObjectDiff.new(uat_doc, prod_doc, engine: engine, strategy: Strategy.new(pairing: :assisted, scope: :data_migration)) diff.changes # => Change, ... diff.pairing # => Pairing::Engine::Result (accepted / ambiguous / unmatched — for review) diff.unresolved # => [field-doc, ...] sources the human must adjudicate (ambiguous+unmatched)

SAFETY — only ACCEPTED pairs are treated as equivalences; ambiguous/unmatched sources are surfaced in unresolved and NEVER auto-paired or auto-removed. A same-label field whose genome contradicts is escalated, not silently matched (the engine guarantees this).

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source_doc, target_doc, engine: nil, strategy: Strategy.new(pairing: :assisted)) ⇒ CrossObjectDiff

Returns a new instance of CrossObjectDiff.

Parameters:

  • source_doc (Hash)

    the "before" template/page (e.g. the desired/UAT state).

  • target_doc (Hash)

    the "after"/destination template/page (e.g. PROD).

  • engine (Pairing::Engine, nil) (defaults to: nil)

    the equivalence matcher; a default one is built when nil. Provide one wired to a Ledger to reuse confirmed pairings and learn over time.

  • strategy (Strategy) (defaults to: Strategy.new(pairing: :assisted))

    scope + move-sensitivity. Pairing is inherently cross-object here; strategy.pairing is informational (which signal set the caller intends).



37
38
39
40
41
42
# File 'lib/ecoportal/api/graphql/diff/cross_object_diff.rb', line 37

def initialize(source_doc, target_doc, engine: nil, strategy: Strategy.new(pairing: :assisted))
  @source   = source_doc || {}
  @target   = target_doc || {}
  @engine   = engine || Pairing::Engine.new
  @strategy = strategy || Strategy.new(pairing: :assisted)
end

Instance Attribute Details

#strategyObject (readonly)

Returns the value of attribute strategy.



29
30
31
# File 'lib/ecoportal/api/graphql/diff/cross_object_diff.rb', line 29

def strategy
  @strategy
end

Instance Method Details

#changelogObject



61
62
63
# File 'lib/ecoportal/api/graphql/diff/cross_object_diff.rb', line 61

def changelog
  changes.map(&:description)
end

#changesObject

Strategy-filtered change-set, emitted against the pairing map (target ids translated into the source id-space so the changes replay coherently through the synthesizer).



57
58
59
# File 'lib/ecoportal/api/graphql/diff/cross_object_diff.rb', line 57

def changes
  @changes ||= @strategy.filter(all_changes)
end

#pairingObject

The Pairing::Engine::Result for the field sets (accepted / ambiguous / unmatched).



45
46
47
# File 'lib/ecoportal/api/graphql/diff/cross_object_diff.rb', line 45

def pairing
  @pairing ||= @engine.pair(source_fields, target_fields)
end

#summaryObject



65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/ecoportal/api/graphql/diff/cross_object_diff.rb', line 65

def summary
  by_op = changes.group_by(&:op)
  {
    added:      by_op.fetch(:added, []).size,
    removed:    by_op.fetch(:removed, []).size,
    changed:    by_op.fetch(:changed, []).size,
    moved:      by_op.fetch(:moved, []).size,
    total:      changes.size,
    paired:     pairing.accepted.size,
    unresolved: unresolved.size
  }
end

#to_hObject



78
79
80
# File 'lib/ecoportal/api/graphql/diff/cross_object_diff.rb', line 78

def to_h
  { summary: summary, changes: changes.map(&:to_h), pairing: pairing.to_h }
end

#unresolvedObject

Source field docs the engine could not confidently pair — the human review set. These are NOT emitted as removals (we do not know they were deleted vs merely unpaired).



51
52
53
# File 'lib/ecoportal/api/graphql/diff/cross_object_diff.rb', line 51

def unresolved
  pairing.ambiguous.map(&:source) + pairing.unmatched
end