Module: Ecoportal::API::GraphQL::Diff::Pairing::Signals

Defined in:
lib/ecoportal/api/graphql/diff/pairing/signals.rb

Overview

Weak pairing SIGNALS combined into a confidence score. Each signal scores a candidate (source-field-doc, target-field-doc) pair in 0.0..1.0. NONE is ground truth on its own — per the domain reference genomeSignature is strong-but-fallible (6 documented failure modes: manual/no-platform-support, new features never added, wrong for select options, wrongly syncs checklists, re-used/re-purposed fields, incomplete data-loss accounting). So genome is ONE signal among several, weighted highest but able to be outvoted / confirmed by type+label+options. The Engine aggregates these with WEIGHTS.

Constant Summary collapse

WEIGHTS =

Relative weights (need not sum to 1; the Engine normalises by the weights that applied).

{
  genome:  0.5,
  type:    0.2,
  label:   0.2,
  options: 0.1
}.freeze

Class Method Summary collapse

Class Method Details

.blank?(value) ⇒ Boolean

Returns:

  • (Boolean)


96
97
98
# File 'lib/ecoportal/api/graphql/diff/pairing/signals.rb', line 96

def blank?(value)
  value.nil? || value.to_s.strip.empty?
end

.genome(source, target) ⇒ Object

genomeSignature match. Strong but fallible, so a MATCH scores high (not 1.0) and a MISMATCH scores 0 rather than vetoing (the field may have been re-purposed keeping its genome, or genome may be absent on newer field types). Returns nil when neither side carries a genome — the signal simply does not apply and is excluded from the average.



28
29
30
31
32
33
34
35
# File 'lib/ecoportal/api/graphql/diff/pairing/signals.rb', line 28

def genome(source, target)
  a = source['genomeSignature']
  b = target['genomeSignature']
  return nil if blank?(a) && blank?(b)
  return 0.0 if blank?(a) || blank?(b)

  a == b ? 1.0 : 0.0
end

.jaccard(list_a, list_b) ⇒ Object

Jaccard similarity of two token collections (|∩| / |∪|). 0.0 when both empty.



86
87
88
89
90
91
92
93
94
# File 'lib/ecoportal/api/graphql/diff/pairing/signals.rb', line 86

def jaccard(list_a, list_b)
  set_a = list_a.to_a.uniq
  set_b = list_b.to_a.uniq
  return 0.0 if set_a.empty? && set_b.empty?

  inter = (set_a & set_b).size.to_f
  union = (set_a | set_b).size
  union.zero? ? 0.0 : inter / union
end

.label(source, target) ⇒ Object

Label similarity — exact (case/space-insensitive) = 1.0, else a token-overlap (Jaccard) score, so Date logged vs Date of sign-off still gets partial credit (a re-purpose candidate the human should adjudicate rather than an auto-accept).



50
51
52
53
54
55
56
57
# File 'lib/ecoportal/api/graphql/diff/pairing/signals.rb', line 50

def label(source, target)
  a = normalise(source['label'])
  b = normalise(target['label'])
  return nil if a.empty? && b.empty?
  return 1.0 if a == b && !a.empty?

  jaccard(a.split, b.split)
end

.normalise(value) ⇒ Object



73
74
75
# File 'lib/ecoportal/api/graphql/diff/pairing/signals.rb', line 73

def normalise(value)
  value.to_s.strip.downcase.gsub(/\s+/, ' ')
end

.option_keys(field) ⇒ Object



77
78
79
80
81
82
83
# File 'lib/ecoportal/api/graphql/diff/pairing/signals.rb', line 77

def option_keys(field)
  Array(field['options']).filter_map do |o|
    next unless o.is_a?(Hash)

    normalise(o['value'] || o['label'] || o['name'])
  end.reject(&:empty?)
end

.options(source, target) ⇒ Object

Select-option overlap by VALUE (primary) / label (auxiliary) — the CORRECT option identity, never genome. Returns nil for non-select fields (no options either side).



61
62
63
64
65
66
67
# File 'lib/ecoportal/api/graphql/diff/pairing/signals.rb', line 61

def options(source, target)
  a = option_keys(source)
  b = option_keys(target)
  return nil if a.empty? && b.empty?

  jaccard(a, b)
end

.type(source, target) ⇒ Object

Field type (__typename) equality. A type change is a strong DISQUALIFIER for a data pairing (you cannot faithfully migrate PlainText data into a Gauge), so a mismatch is 0.



39
40
41
42
43
44
45
# File 'lib/ecoportal/api/graphql/diff/pairing/signals.rb', line 39

def type(source, target)
  ta = type_of(source)
  tb = type_of(target)
  return nil if ta.nil? && tb.nil?

  ta == tb ? 1.0 : 0.0
end

.type_of(field) ⇒ Object



69
70
71
# File 'lib/ecoportal/api/graphql/diff/pairing/signals.rb', line 69

def type_of(field)
  field['__typename'] || field['type']
end