Module: Varar::Core::Drifts
- Defined in:
- lib/varar/core/drift.rb
Overview
Spec drift detection: a paragraph the committed varar.lock.json baseline recorded as an example that now matches no step. Pure, byte-identical to the TS port so varar.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
- .derive_spec_baseline(source, var_doc, plan) ⇒ Object
-
.detect_drift(baseline, var_doc, plan) ⇒ Object
Paragraphs the baseline recorded as examples that now match zero steps.
- .drift_diagnostics(drifts) ⇒ Object
- .live?(candidate_span, plan) ⇒ Boolean
-
.live_examples(var_doc, plan) ⇒ Object
The current example-producing paragraphs, in document order.
- .parse_baseline_example(value) ⇒ Object
- .parse_spec_baseline(value) ⇒ Object
- .parse_var_lock(text) ⇒ Object
-
.reconcile_drift(store, spec_path, source, var_doc, plan, update: false) ⇒ Object
One spec's baseline reconciliation against a BaselineStore.
-
.similarity(set_a, set_b) ⇒ Object
Jaccard overlap |A∩B| / |A∪B|.
-
.stringify_var_lock(lock) ⇒ Object
Serialize varar.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.
-
.tokenize(text) ⇒ Object
Lower-cased word tokens (letters/digits) — the unit of similarity.
- .within?(inner, outer) ⇒ Boolean
Class Method Details
.derive_spec_baseline(source, var_doc, plan) ⇒ Object
65 66 67 |
# File 'lib/varar/core/drift.rb', line 65 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).
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 97 98 99 100 |
# File 'lib/varar/core/drift.rb', line 72 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
102 103 104 |
# File 'lib/varar/core/drift.rb', line 102 def drift_diagnostics(drifts) drifts.map { |d| Diagnostics.drift_detected(d.name, d.span) } end |
.live?(candidate_span, plan) ⇒ Boolean
38 39 40 |
# File 'lib/varar/core/drift.rb', line 38 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.
57 58 59 60 61 62 63 |
# File 'lib/varar/core/drift.rb', line 57 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
159 160 161 162 163 164 165 166 167 |
# File 'lib/varar/core/drift.rb', line 159 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
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/varar/core/drift.rb', line 142 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
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/varar/core/drift.rb', line 123 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).
110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/varar/core/drift.rb', line 110 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.
48 49 50 51 52 53 54 |
# File 'lib/varar/core/drift.rb', line 48 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 varar.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.
172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/varar/core/drift.rb', line 172 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.
43 44 45 |
# File 'lib/varar/core/drift.rb', line 43 def tokenize(text) text.downcase.scan(TOKEN_RE).to_set end |
.within?(inner, outer) ⇒ Boolean
34 35 36 |
# File 'lib/varar/core/drift.rb', line 34 def within?(inner, outer) inner.start_offset >= outer.start_offset && inner.end_offset <= outer.end_offset end |