Module: Oselvar::Var::Core::Drifts

Defined in:
lib/oselvar/var/core/drift.rb

Overview

Spec drift detection: a paragraph the committed var.lock.json baseline recorded as an example that now matches no step. Pure, byte-identical to the TS port so var.lock.json is shared across languages. Port of drift.ts.

BaselineStore is a duck-typed port: #read -> String|nil, #write(contents).

Constant Summary collapse

SIMILARITY_THRESHOLD =

A paragraph may be moved anywhere and reworded up to ~half its words and still be recognized; edit it past this and it reads as remove+add, not drift. Ported byte-identically.

0.5
TOKEN_RE =
/[[:alnum:]]+/

Class Method Summary collapse

Class Method Details

.derive_spec_baseline(source, var_doc, plan) ⇒ Object



66
67
68
# File 'lib/oselvar/var/core/drift.rb', line 66

def derive_spec_baseline(source, var_doc, plan)
  SpecBaseline.new(source_hash: Hash32.hash_source(source), examples: live_examples(var_doc, plan))
end

.detect_drift(baseline, var_doc, plan) ⇒ Object

Paragraphs the baseline recorded as examples that now match zero steps. Each re-identified by the most word-similar current paragraph at/above the threshold (exact name scores 1; ties break toward the nearest line).



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/oselvar/var/core/drift.rb', line 73

def detect_drift(baseline, var_doc, plan)
  return [] if baseline.nil?

  candidates = var_doc.examples
  tokens = candidates.map { |c| tokenize(Plan.derive_example_name(c.body)) }
  live = candidates.map { |c| live?(c.span, plan) }

  baseline.examples.filter_map do |b|
    b_tokens = tokenize(b.name)
    best_idx = -1
    best_score = 0.0
    candidates.each_with_index do |candidate, i|
      score = similarity(b_tokens, tokens[i])
      next if score < SIMILARITY_THRESHOLD

      line = candidate.span.start_line
      best_line = best_idx >= 0 ? candidates[best_idx].span.start_line : 0
      next unless best_idx.negative? || score > best_score ||
                  (score == best_score && (line - b.line).abs < (best_line - b.line).abs)

      best_idx = i
      best_score = score
    end
    next if best_idx.negative?
    next if live[best_idx]

    Drift.new(name: b.name, line: candidates[best_idx].span.start_line, span: candidates[best_idx].span)
  end
end

.drift_diagnostics(drifts) ⇒ Object



103
104
105
# File 'lib/oselvar/var/core/drift.rb', line 103

def drift_diagnostics(drifts)
  drifts.map { |d| Diagnostics.drift_detected(d.name, d.span) }
end

.live?(candidate_span, plan) ⇒ Boolean

Returns:

  • (Boolean)


39
40
41
# File 'lib/oselvar/var/core/drift.rb', line 39

def live?(candidate_span, plan)
  plan.examples.any? { |pe| within?(pe.span, candidate_span) }
end

.live_examples(var_doc, plan) ⇒ Object

The current example-producing paragraphs, in document order.



58
59
60
61
62
63
64
# File 'lib/oselvar/var/core/drift.rb', line 58

def live_examples(var_doc, plan)
  var_doc.examples.filter_map do |candidate|
    next unless live?(candidate.span, plan)

    BaselineExample.new(name: Plan.derive_example_name(candidate.body), line: candidate.span.start_line)
  end
end

.parse_baseline_example(value) ⇒ Object



160
161
162
163
164
165
166
167
168
# File 'lib/oselvar/var/core/drift.rb', line 160

def parse_baseline_example(value)
  return nil unless value.is_a?(::Hash)

  name = value['name']
  line = value['line']
  return nil unless name.is_a?(String) && line.is_a?(Integer)

  BaselineExample.new(name: name, line: line)
end

.parse_spec_baseline(value) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/oselvar/var/core/drift.rb', line 143

def parse_spec_baseline(value)
  return nil unless value.is_a?(::Hash)

  source_hash = value['sourceHash']
  examples_raw = value['examples']
  return nil unless source_hash.is_a?(String) && examples_raw.is_a?(Array)

  examples = []
  examples_raw.each do |item|
    parsed = parse_baseline_example(item)
    return nil if parsed.nil?

    examples << parsed
  end
  SpecBaseline.new(source_hash: source_hash, examples: examples)
end

.parse_var_lock(text) ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/oselvar/var/core/drift.rb', line 124

def parse_var_lock(text)
  parsed = JSON.parse(text)
  return nil unless parsed.is_a?(::Hash) && parsed['version'] == 1

  specs_raw = parsed['specs']
  return nil unless specs_raw.is_a?(::Hash)

  specs = {}
  specs_raw.each do |path, value|
    baseline = parse_spec_baseline(value)
    return nil if baseline.nil?

    specs[path] = baseline
  end
  VarLock.new(version: 1, specs: specs)
rescue JSON::ParserError, TypeError
  nil
end

.reconcile_drift(store, spec_path, source, var_doc, plan, update: false) ⇒ Object

One spec's baseline reconciliation against a BaselineStore. In update mode, accept all drift (re-record, report nothing); otherwise detect drift and rewrite the baseline only on a clean run, so an unacknowledged drift keeps its old entry (and stays red).



111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/oselvar/var/core/drift.rb', line 111

def reconcile_drift(store, spec_path, source, var_doc, plan, update: false)
  text = store.read
  lock = text ? parse_var_lock(text) : nil
  baseline = lock ? lock.specs[spec_path] : nil
  drifts = update ? [] : detect_drift(baseline, var_doc, plan)
  if update || drifts.empty?
    specs = lock ? lock.specs.dup : {}
    specs[spec_path] = derive_spec_baseline(source, var_doc, plan)
    store.write(stringify_var_lock(VarLock.new(version: 1, specs: specs)))
  end
  drifts
end

.similarity(set_a, set_b) ⇒ Object

Jaccard overlap |A∩B| / |A∪B|. 1 identical, 0 disjoint; two empty = 1.



49
50
51
52
53
54
55
# File 'lib/oselvar/var/core/drift.rb', line 49

def similarity(set_a, set_b)
  return 1.0 if set_a.empty? && set_b.empty?

  intersection = (set_a & set_b).size
  union = set_a.size + set_b.size - intersection
  union.zero? ? 0.0 : intersection.to_f / union
end

.stringify_var_lock(lock) ⇒ Object

Serialize var.lock.json deterministically: spec paths sorted, examples in document order, insertion-order keys otherwise (version, specs; sourceHash, examples; name, line) — NOT canonical JSON's key sort.



173
174
175
176
177
178
179
180
181
182
183
# File 'lib/oselvar/var/core/drift.rb', line 173

def stringify_var_lock(lock)
  specs = {}
  lock.specs.keys.sort.each do |path|
    baseline = lock.specs[path]
    specs[path] = {
      'sourceHash' => baseline.source_hash,
      'examples' => baseline.examples.map { |e| { 'name' => e.name, 'line' => e.line } }
    }
  end
  CanonicalJson.ordered_stringify({ 'version' => 1, 'specs' => specs })
end

.tokenize(text) ⇒ Object

Lower-cased word tokens (letters/digits) — the unit of similarity.



44
45
46
# File 'lib/oselvar/var/core/drift.rb', line 44

def tokenize(text)
  text.downcase.scan(TOKEN_RE).to_set
end

.within?(inner, outer) ⇒ Boolean

Returns:

  • (Boolean)


35
36
37
# File 'lib/oselvar/var/core/drift.rb', line 35

def within?(inner, outer)
  inner.start_offset >= outer.start_offset && inner.end_offset <= outer.end_offset
end