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
- .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 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.
-
.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
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
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
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 |